1 /*
2  *  Copyright (C) 1998-2000 by Marco G"otze.
3  *
4  *  This code is part of the wmpinboard source package, which is
5  *  distributed under the terms of the GNU GPL2.
6  */
7 
8 #include <ctype.h>
9 #include <getopt.h>
10 #include <stdlib.h>
11 #include <stdio.h>
12 #include <time.h>
13 #include <sys/types.h>
14 #include <sys/stat.h>
15 #include <sys/time.h>
16 
17 #include <X11/Xlib.h>
18 #include <X11/Xutil.h>
19 #include <X11/cursorfont.h>
20 #include <X11/keysym.h>
21 #include <X11/xpm.h>
22 #include <X11/extensions/shape.h>
23 
24 #include "wmpinboard.h"
25 #include "misc.h"
26 #include "notes.h"
27 #include "xmisc.h"
28 
29 #ifdef HAVE_SIGNAL_H
30 #include <signal.h>
31 #endif
32 #ifdef HAVE_STRING_H
33 #include <string.h>
34 #endif
35 #ifdef HAVE_UNISTD_H
36 #include <unistd.h>
37 #endif
38 
39 #include "../xpm/pinboard.xpm"
40 #include "../xpm/bbar.xpm"
41 #include "../xpm/abar.xpm"
42 #include "../xpm/digits.xpm"
43 
44 const char *rc_file_name = "/.wmpinboarddata";  /* to be prepended with dir */
45 
46 const char c_group[C_NUM] =  /* group available note colors */
47   { 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 0, 0, 0 };
48 
49 #define NUM_FONTS 3
50 const struct {  /* shortcuts for known fonts */
51   char name[8];
52   char desc[40];
53   char font[60];
54 } fonts[NUM_FONTS] = {
55   { "Default", "default font (ISO8859-1)", "-*-fixed-*--10-*-iso8859-1" },
56   { "Latin2",  "Latin2 (ISO8859-2)",       "-*-fixed-*--10-*-iso8859-2" },
57   { "Russian", "cyrillic font (KOI8-R)",   "-*-fixed-*--10-*-koi8-r" }
58 };
59 const char *default_font = fonts[0].font;
60 
61 opts_t opts = { 0, 0, 0, -1, TIMEOUT, 1, "", "", "" };
62 
63 palette_t palette[C_NUM+1];
64 Cursor cursors[3];  /* alternate X cursors */
65 
66 int label_coords[4] = { 12, 0, 39, 9 };  /* default "TO DO" label coords */
67 
68 data_t ndata[MAX_NOTES];  /* this holds all the notes */
69 int notes_count = 0;
70 
71 state_t state = { 0, { 0, 0 }, 0, -1, -1, 0, 0, M_NOOP, -1, -1, 0, 0, 0, 0, 0,
72   0, 0, 0, 0, 0, 0, 0, { -1, -1, 0, 0, None } };
73 
74 Display *display;
75 Window mainwin, iconwin, win;  /* win = visible window */
76 Pixmap app = None, bbar = None, abar = None, digits = None;
77 XImage *img = 0;
78 GC normalGC = 0, fontGC = 0, fillGC = 0;
79 #ifdef CREASES
80 GC creaseGC = 0;
81 #endif
82 XFontStruct *font = 0;
83 
84 sigset_t sync_sig_set;  /* signals used for syncing with other instances */
85 int sync_sig_block_counter = 0;
86 
87                                /*************/
88 /******************************* FUNCTIONS ***********************************/
89                              /*************/
90 
91 void s_block();
92 void s_unblock();
93 int  notes_io(int);
94 void load_theme(const char*);
95 void init(void);
96 void done(void);
97 int  try_font(const char*, int);
98 void load_font(void);
99 void set_kbfocus(int);
100 void action(actions, const void*);
101 void help(void);
102 void parse_argv(int, char**);
103 void draw_cursor(int, int);
104 void set_cursor(int, int);
105 void sel_text(int);
106 void draw_pixel(int, int);
107 void set_mode(modes);
108 void quit_edit_mode(int, int);
109 void timer(unsigned int);
110 void animate_bbar(int);
111 void animate_abar(int);
112 void animate_note(int);
113 void animate_alarm();
114 void handle_ButtonPress(XEvent*);
115 void handle_ButtonRelease(XEvent*);
116 void handle_MotionNotify(XEvent*);
117 void handle_KeyPress(XEvent*);
118 void handle_sigs(int);
119 
120 /*
121  * blocks the signals in sync_sig_set
122  */
123 inline void
s_block()124 s_block()
125 {
126   if (!sync_sig_block_counter++)
127     sigprocmask(SIG_BLOCK, &sync_sig_set, 0);
128 }
129 
130 /*
131  * inverse of the above
132  */
133 inline void
s_unblock()134 s_unblock()
135 {
136   if (!--sync_sig_block_counter)
137     sigprocmask(SIG_UNBLOCK, &sync_sig_set, 0);
138 }
139 
140 /*
141  * reads or writes the data file; returns the PID the file was last written by
142  *
143  * file format revisions:
144  *   0 = v0.7+: introduced binary data file format
145  *   1 = v0.8+: added <data_t.{cursor,sketch}>
146  *   2 = v0.9+: added <data_t.creases>, <state.state_bits>
147  *   3 = v0.9.1+: added <PID>
148  *   4 = v0.10+: added <theme>
149  *   5 = v0.99: added <counter>
150  *   6 = v0.99.1: added <alarm_cmd>, <data_t.a_{time,flags}>
151  */
152 int
notes_io(int save)153 notes_io(int save)
154 {
155   /* size of per-note records for specific revisions */
156   static const int size_0 = sizeof(struct { int a, b, c; char d[60]; });
157   static const int size_1 =
158     sizeof(struct { int a, b, c; char d[60]; int e; char f[512]; });
159   static const int size_2 =
160     sizeof(struct { int a, b, c; char d[60]; int e; char f[512]; char g[32]; });
161   static const int size_6 = sizeof(struct { int a, b, c; char d[60]; int e;
162     char f[512]; char g[32]; time_t h; char i; });
163   static const char *ext = ".new";
164   static const char *header = "WMPB6";  /* data file header */
165   char s[STRING_BUF_SIZE];
166   char t[STRING_BUF_SIZE];
167   FILE *file;
168   int pid = (int) getpid();
169   static int sizes[7];
170   sizes[0] = size_0;
171   sizes[1] = size_1;
172   sizes[2] = size_2;
173   sizes[3] = size_2;
174   sizes[4] = size_2;
175   sizes[5] = size_2;
176   sizes[6] = size_6;
177 
178   s_block();
179 
180   s[sizeof(s)-1] = '\0';
181   strncpy(s, getenv("HOME"), sizeof(s));
182   if (sizeof(s)-strlen(s)-1 < strlen(rc_file_name)+strlen(ext))
183     die("Buffer too small in notes_io().");
184   strcat(s, rc_file_name);
185   if (save) {  /* save to temporary file first, later rename it */
186     strcpy(t, s);
187     strcat(s, ext);
188   }
189 
190   file = fopen(s, save ? "wb" : "rb");
191   if (!file) {
192     if (save)
193       die("Couldn't open data file for writing.");
194     else {
195       s_unblock();
196       return pid;  /* just don't read in anything if opening the file failed */
197     }
198   }
199 
200   if (save) {  /*** SAVE ***/
201     fwrite(header, 5, 1, file);
202     fputs(opts.font, file);
203     fputc('\n', file);
204     fputs(opts.theme, file);
205     fputc('\n', file);
206     fputs(opts.alarm_cmd, file);
207     fputc('\n', file);
208     fwrite(&pid, sizeof(pid), 1, file);
209     fwrite(&state.state_bits, sizeof(state.state_bits), 1, file);
210     fwrite(&state.counter, sizeof(state.counter), 1, file);
211     fwrite(ndata, sizeof(data_t), notes_count, file);
212     fclose(file);
213 
214     strcpy(s, t);
215     strcat(s, ext);
216     if (rename(s, t) == -1) {
217       fprintf(stderr, "Fatal error: Failed to rename file `%s' to `%s'.\n", t,
218         s);
219       exit(EXIT_FAILURE);
220     }
221   } else {  /*** LOAD ***/
222     int rev;
223 
224     if (!fread(s, 5, 1, file) || strncmp(s, header, 4)) {  /* check header */
225       fprintf(stderr, "Fatal error: Corrupt data file encountered.\n"
226         "Delete `~%s' to start over.\n", rc_file_name);
227       fclose(file);
228       exit(EXIT_FAILURE);
229     }
230     /* file format revision check */
231     s[5] = '\0';
232     rev = strtol(&s[4], 0, 10);
233     if (rev > 6) {
234       fprintf(stderr, "Fatal error: The data file seems to have been created "
235         "by a more recent version\nof wmpinboard.  Try and find a newer "
236         "release, or remove `~%s'\nand lose all existing notes.\n",
237         rc_file_name);
238       fclose(file);
239       exit(EXIT_FAILURE);
240     }
241 
242     fgets(opts.font, sizeof(opts.font), file);
243     if (strlen(opts.font) && opts.font[strlen(opts.font)-1] == '\n')
244       opts.font[strlen(opts.font)-1] = '\0';
245 
246     if (rev >= 4) {
247       fgets(opts.theme, sizeof(opts.theme), file);
248       if (strlen(opts.theme) && opts.theme[strlen(opts.theme)-1] == '\n')
249         opts.theme[strlen(opts.theme)-1] = '\0';
250     }
251     if (rev >= 6) {
252       fgets(opts.alarm_cmd, sizeof(opts.alarm_cmd), file);
253       if (strlen(opts.alarm_cmd) && opts.alarm_cmd[strlen(opts.alarm_cmd)-1] ==
254         '\n')
255       {
256         opts.alarm_cmd[strlen(opts.alarm_cmd)-1] = '\0';
257       }
258     }
259     if (rev >= 3) fread(&pid, sizeof(pid), 1, file);  /* last writer's PID */
260     if (rev >= 2)  /* state.state_bits */
261       fread(&state.state_bits, sizeof(state.state_bits), 1, file);
262     else
263       state.state_bits = 0;
264     if (rev >= 5) /* counter */
265       fread(&state.counter, sizeof(state.counter), 1, file);
266     else
267       state.counter = 0;
268 
269     notes_count = 0;
270     while (notes_count < MAX_NOTES)
271       if (fread(&ndata[notes_count], sizes[rev], 1, file)) {
272         switch (rev) {
273           case 0:
274             ndata[notes_count].cursor = 0;
275             memset(ndata[notes_count].sketch, 0, 512);
276           case 1:
277             memset(ndata[notes_count].creases, 0, 32);
278           case 2:
279           case 3:
280           case 4:
281           case 5:
282             ndata[notes_count].a_time = -1;
283             ndata[notes_count].a_flags = ALARM_DATE;
284           default:
285             notes_count++;
286         }
287       } else break;
288     if (!state.counter) state.counter = notes_count;
289     fclose(file);
290     state.alarm.time = -1;
291     state.alarm.note = -1;
292     time_next_alarm();
293   }
294 
295   s_unblock();
296   return pid;
297 }
298 
299 /*
300  * loads theme from <filename>
301  */
302 void
load_theme(const char * filename)303 load_theme(const char *filename)
304 {
305 #ifdef LOW_COLOR
306   if (strlen(filename))
307     WARN("Skipped loading the configured theme since themes aren't\nsupported"
308       "at low color depths.");
309 #else
310   int coords[4], eof = 0, i;
311   char s[STRING_BUF_SIZE], *t, *p, *q;
312   struct stat buf;
313   FILE *file;
314 
315   if (!strlen(filename)) return;
316   file = fopen(filename, "r");
317   if (!file) {
318     WARN("Failed to open theme file; check whether the location you specified\n"
319       "is valid.  Reverting to default.");
320     return;
321   }
322   if (!fgets(s, sizeof(s), file) || strncmp(s, "WMPBtheme", 9)) {
323     fclose(file);
324     WARN("Configured theme file is corrupted!  Reverting to default.");
325     return;
326   }
327 
328   /* we'll need at most <file size> bytes to buffer the XPM data read from it */
329   lstat(filename, &buf);
330   if (!(t = malloc(buf.st_size))) {
331     fclose(file);
332     WARN("Skipped loading configured theme due to memory shortage.");
333     return;
334   }
335 
336   /* parse theme headers... */
337   if (s[strlen(s)-1] == '\n') s[strlen(s)-1] = '\0';
338   memset(s, ' ', 9);  /* kludge */
339   do {
340     if ((p = strchr(s, '#'))) *p = '\0';  /* # comments */
341     p = s;
342     while (*p) {
343       for (; *p == ' ' || *p == '\t'; p++);
344       for (q = p; isalpha(*q); q++);
345       if (q > p) {
346         if (!strncasecmp(p, "label", q-p)) {
347           for (p = q; *p == ' ' || *p == '\t'; p++);
348           if (*p == '=') {
349             for (p++; *p == ' ' || *p == '\t'; p++);
350             for (i = 0; *p && i < 4; i++) {
351               coords[i] = (int) strtol(p, &p, 10);
352               for (; *p && (*p < '0' || *p > '9'); p++);
353             }
354             q = p;
355             if (i >= 4)
356               if (
357                 coords[0] < 0 || coords[0] > 51 ||
358                 coords[2] < 0 || coords[2] > 51 ||
359                 coords[1] < 0 || coords[1] > 59 ||
360                 coords[3] < 0 || coords[3] > 59 ||
361                 coords[0] >= coords[2] || coords[1] >= coords[3])
362               {
363                 WARN("Theme header `label' followed by coordinates outside the"
364                   "\nallowed range--ignored.");
365               } else {
366                 if ((coords[2]-coords[0])*(coords[3]-coords[1]) >= 16)
367                   memcpy(&label_coords, &coords, sizeof(coords));
368                 else
369                   WARN("Label Label area defined by the configured theme's "
370                     "headers is too small--ignored.");
371               }
372             else
373               WARN("Theme header `label' followed by improper specification--"
374                 "ignored.");
375           } else {
376             WARN("Theme header `label' lacks specification.");
377             q = p;
378           }
379         } else
380           fprintf(stderr, "Warning: Invalid header field in theme: `%s'.\n", p);
381       }
382       for (p = q; *p && *p != ' ' && *p != '\t'; p++);
383     }
384     eof = !fgets(s, sizeof(s), file);
385     if (strlen(s) && s[strlen(s)-1] == '\n') s[strlen(s)-1] = '\0';
386   } while (!eof && strlen(s));
387 
388   /* now read all pixmap data sections from the remainder of the file... */
389   while (!eof) {
390     Pixmap pic;
391     XpmAttributes attr;
392 
393     attr.valuemask = XpmExactColors | XpmCloseness;
394     attr.exactColors = False;
395     attr.closeness = 65536;
396     for (p = t; !(eof = !fgets(s, sizeof(s), file)) && strlen(s) && *s != '\n';)
397     {
398       strcpy(p, s);
399       p += strlen(s);
400     }
401     if (!strstr(t, "char")) continue;  /* silently ignore garbage */
402     if (XpmCreatePixmapFromBuffer(display, RootWindow(display,
403       DefaultScreen(display)), t, &pic, 0, &attr) == XpmSuccess)
404     {
405       if (attr.width == 52 && attr.height == 60)  /* board pixmap */
406         XGetSubImage(display, pic, 0, 0, 52, 60, ~0, ZPixmap, img, 6, 2);
407       else if (attr.width == 58 && attr.height == 30)  /* bbar pixmap */
408         XCopyArea(display, pic, bbar, normalGC, 0, 0, 58, 30, 0, 0);
409       else if (attr.width == 58 && attr.height == 28)  /* abar pixmap */
410         XCopyArea(display, pic, abar, normalGC, 0, 0, 58, 28, 0, 0);
411       else if (attr.width == 60 && attr.height == 9)  /* abar digits */
412         XCopyArea(display, pic, digits, normalGC, 0, 0, 60, 9, 0, 0);
413       else /* invalid pixmap */
414         WARN("Configured theme contains pixmap of invalid dimensions--"
415           "ignored.");
416       XFreePixmap(display, pic);
417     } else
418       WARN("Encountered invalid pixmap data in configured theme--ignored.");
419   }
420   free(t);
421 #endif
422 }
423 
424 /*
425  * tries to exit properly
426  */
427 void
done(void)428 done(void)
429 {
430   if (normalGC) XFreeGC(display, normalGC);
431   if (fontGC) XFreeGC(display, fontGC);
432   if (fillGC) XFreeGC(display, fillGC);
433 #ifdef CREASES
434   if (creaseGC) XFreeGC(display, creaseGC);
435 #endif
436   if (font) XFreeFont(display, font);
437   if (app != None) XFreePixmap(display, app);
438   if (bbar != None) XFreePixmap(display, bbar);
439   if (abar != None) XFreePixmap(display, abar);
440   if (digits != None) XFreePixmap(display, digits);
441   if (state.alarm.buffer != None) XFreePixmap(display, state.alarm.buffer);
442   if (img) XDestroyImage(img);
443   XCloseDisplay(display);
444   if (opts.display) free(opts.display);
445 }
446 
447 /*
448  * handler for caught signals
449  */
450 void
handle_sigs(int sig)451 handle_sigs(int sig)
452 {
453   /* note: the kludges below will result in the application being terminated
454      if the applet was destroyed via the respective WM option but not yet
455      terminated (apparently WM just destroys the window in this case) */
456   XWindowAttributes attr;
457   const char *s = 0;
458 
459   s_block();
460 
461   switch (sig) {
462     case SIGALRM:  /* used in animation timing */
463       state.alarmed = 1;
464       break;
465     case SIGUSR1:
466       XGetWindowAttributes(display, win, &attr);  /* kludge */
467       /* quit edit mode w/saving; if not in edit mode, just save */
468       if (state.mode == M_MOVE) set_mode(M_NOOP);
469       if (state.mode != M_NOOP)
470         quit_edit_mode(0, 1);  /* saves */
471       else
472         notes_io(1);
473       break;
474     case SIGUSR2:
475       XGetWindowAttributes(display, win, &attr);  /* kludge */
476       /* quit edit mode w/o saving... */
477       if (state.mode != M_NOOP && state.mode != M_MOVE) quit_edit_mode(0, 0);
478       notes_io(0);  /* reread data */
479       notes_io(1);  /* rewrite data file */
480       render_pinboard(-1);
481       redraw_window();
482       break;
483     case SIGINT:
484       s = "SIGINT";
485     case SIGTERM:
486       if (!s) s = "SIGTERM";
487       fprintf(stderr, "Caught %s.  Trying to exit cleanly...\n", s);
488       if (state.mode != M_NOOP) {
489         if (note_empty(state.cur_note)) remove_note(state.cur_note);
490         notes_io(1);
491       }
492       exit(EXIT_SUCCESS);
493   }
494 
495   s_unblock();
496 }
497 
498 /*
499  * initializes the application's X window, installs a signal handler
500  */
501 void
init(void)502 init(void)
503 {
504   struct sigaction sigact;
505   XWMHints wmhints;
506   XSizeHints shints;
507   Atom atom;
508   XTextProperty name;
509   XGCValues gcv;
510   unsigned long gcm;
511   int screen;
512 
513   /* set up syncsig_set */
514   sigemptyset(&sync_sig_set);
515   sigaddset(&sync_sig_set, SIGUSR1);
516   sigaddset(&sync_sig_set, SIGUSR2);
517   /* install signal handler */
518   sigact.sa_handler = handle_sigs;
519   sigemptyset(&sigact.sa_mask);
520   sigact.sa_flags = 0;
521   if (sigaction(SIGALRM, &sigact, 0) < 0 ||
522     sigaction(SIGUSR1, &sigact, 0) < 0 ||
523     sigaction(SIGUSR2, &sigact, 0) < 0 ||
524     sigaction(SIGINT, &sigact, 0) < 0 ||
525     sigaction(SIGTERM, &sigact, 0) < 0)
526   {
527     die("Unable to install signal handlers.");
528   }
529 
530   if (!(display = XOpenDisplay(opts.display))) {
531     fprintf(stderr, "Fatal error: Can't open display `%s'.\n",
532       XDisplayName(opts.display));
533     exit(EXIT_FAILURE);
534   }
535 
536   /* try to set up done() to be called upon exit() */
537   if (atexit(done)) die("Failed to set up exit procedure.");
538 
539   screen = DefaultScreen(display);
540 
541   atom = XInternAtom(display, "WM_DELETE_WINDOW", 0);
542   if (atom == None) die("No window manager running.");
543 
544   /* try to autodetect whether we're running Window Maker... */
545   if (opts.window_state < 0)  /* no window state explicitly specified */
546     opts.window_state = XInternAtom(display, "_WINDOWMAKER_WM_FUNCTION", 1) !=
547       None ? WithdrawnState : NormalState;
548 
549   mainwin = create_win();
550   wmhints.window_group = mainwin;
551   wmhints.initial_state = opts.window_state;
552   wmhints.flags = StateHint | WindowGroupHint | IconWindowHint;
553   if (opts.animate) wmhints.flags |= XUrgencyHint;  /* of any use at all? */
554   if (opts.window_state == WithdrawnState) {
555     iconwin = create_win();
556     wmhints.icon_window = iconwin;
557     win = iconwin;
558   } else {
559     wmhints.icon_window = None;
560     win = mainwin;
561   }
562   XSetWMHints(display, mainwin, &wmhints);
563   XSetWMProtocols(display, mainwin, &atom, 1);
564 
565   shints.min_width = 64;
566   shints.min_height = 64;
567   shints.max_width = 64;
568   shints.max_height = 64;
569   shints.x = 0;
570   shints.y = 0;
571   shints.flags = PMinSize | PMaxSize | USPosition;
572   XSetWMNormalHints(display, mainwin, &shints);
573 
574   XSelectInput(display, win, ButtonPressMask | ExposureMask |
575     ButtonReleaseMask | PointerMotionMask | StructureNotifyMask |
576     KeyPressMask | EnterWindowMask | LeaveWindowMask | FocusChangeMask);
577   if (!XStringListToTextProperty(&opts.name, 1, &name))
578     die("Can't allocate window name.");
579   XSetWMName(display, mainwin, &name);
580 
581   set_mask(1);
582   app = get_xpm((char**) pinboard_xpm);
583   XMapSubwindows(display, win);
584 
585   gcm = GCForeground | GCBackground | GCGraphicsExposures;
586   gcv.foreground = WhitePixel(display, screen);
587   gcv.background = BlackPixel(display, screen);
588   gcv.graphics_exposures = 0;
589   normalGC = XCreateGC(display, RootWindow(display, screen), gcm, &gcv);
590 }
591 
592 /*
593  * tries to load font name, dies upon an error if <fatal> is true
594  */
595 int
try_font(const char * name,int fatal)596 try_font(const char *name, int fatal)
597 {
598   if (font) return 1;
599   if (!name || !*name) return 0;
600   font = XLoadQueryFont(display, name);
601   if (font) {
602     if (font->max_bounds.rbearing - font->min_bounds.lbearing > 7 ||
603       font->max_bounds.ascent + font->max_bounds.descent > 10)
604     {
605       fprintf(stderr, "Warning: The font `%s'\n"
606         "lacks a fixed character cell size of 6x10.\n", name);
607       XFreeFont(display, font);
608       font = 0;
609     } else {
610       if (!notes_count)
611         strncpy(opts.font, name, sizeof(opts.font));
612       return 1;
613     }
614   } else
615     fprintf(stderr, "Warning: The font `%s' doesn't exist.\n", name);
616   if (fatal)
617     die("No alternatives left, aborting.");
618   else
619     fprintf(stderr, "Trying to revert...\n");
620   return 0;
621 }
622 
623 /*
624  * loads the font to be used
625  */
626 void
load_font(void)627 load_font(void)
628 {
629   XGCValues gcv;
630   unsigned long gcm;
631 
632   try_font(opts.font, 0);
633   try_font(default_font, 1);
634 
635   gcm = GCForeground | GCBackground | GCFillStyle | GCLineStyle | GCFont |
636     GCGraphicsExposures;
637   gcv.foreground = C_OUTER;
638   gcv.background = C_INNER;
639   gcv.font = font->fid;
640   gcv.fill_style = FillSolid;
641   gcv.line_style = LineSolid;
642   gcv.graphics_exposures = 0;
643   fontGC = XCreateGC(display, app, gcm, &gcv);
644 #ifdef CREASES
645   gcm ^= GCBackground | GCFont;
646   creaseGC = XCreateGC(display, app, gcm, &gcv);
647 #endif
648 }
649 
650 /*
651  * (un)sets the keyboard input focus to wmpinboard's window
652  */
653 void
set_kbfocus(int get_it)654 set_kbfocus(int get_it)
655 {
656   if (get_it)
657     XSetInputFocus(display, win, RevertToPointerRoot, CurrentTime);
658   else if (!opts.click_to_focus)
659     XSetInputFocus(display, PointerRoot, RevertToNone, CurrentTime);
660 }
661 
662 /*
663  * takes an action as specified by <type> (for command-line options affection
664  * notes rather than interactive behavior)
665  */
666 void
action(actions type,const void * data)667 action(actions type, const void *data)
668 {
669   const char *s;
670   int pid, running;
671   int i, j, k;
672   FILE *file;
673 
674   pid = notes_io(0);
675   running = flush_instance(pid);
676   if (running) pid = notes_io(0);
677   switch (type) {
678     case C_FONT:
679       if (running) die("Can't change font while another instance is running.");
680       if (notes_count) die("Can't change font if the pinboard isn't empty.");
681       for (i = 0; i < NUM_FONTS; i++)
682         if (!strcasecmp(data, fonts[i].name)) {
683           strcpy(opts.font, fonts[i].font);
684           break;
685         }
686       if (i >= NUM_FONTS) {  /* not a predefined font */
687         if (strlen(data) >= sizeof(opts.font)) {
688           /* avoid trouble when retrieving saved data... */
689           fprintf(stderr, "Fatal error: Specified font descriptor exceeds "
690             "buffer size of %d bytes.\n", sizeof(opts.font));
691           exit(EXIT_FAILURE);
692         }
693         strcpy(opts.font, data);
694       }
695       notes_io(1);
696       fprintf(stderr, "Successfully changed the font.\n");
697       break;
698     case C_THEME:
699       if (running) die("Can't change theme while another instance is running.");
700       if (!strlen(data) || !strcasecmp(data, "default")) {
701         opts.theme[0] = '\0';
702         fprintf(stderr, "Reverted to default theme.\n");
703       } else {
704         if (strlen(data) >= sizeof(opts.theme)) {
705           fprintf(stderr, "Fatal error: Specified theme file location exceeds "
706             "buffer size of %d bytes.\n", sizeof(opts.theme));
707           exit(EXIT_FAILURE);
708         }
709         strcpy(opts.theme, data);
710         if (!(file = fopen(opts.theme, "r")))
711           WARN("Can't open specified theme file.");
712         else
713           fclose(file);
714         fprintf(stderr, "Successfully configured for specified theme.\n");
715       }
716       notes_io(1);
717       break;
718     case C_ALARM_CMD:
719       if (!strlen(data)) {
720         opts.alarm_cmd[0] = '\0';
721         fprintf(stderr, "Disabled alarm command.\n");
722       } else {
723         if (strlen(data) >= sizeof(opts.alarm_cmd)) {
724           fprintf(stderr, "Fatal error: Specified theme file location exceeds "
725             "buffer size of %d bytes.\n", sizeof(opts.alarm_cmd));
726           exit(EXIT_FAILURE);
727         }
728         strcpy(opts.alarm_cmd, data);
729         fprintf(stderr, "Successfully configured specified alarm command.\n");
730       }
731       notes_io(1);
732       if (running) {
733         kill(pid, SIGUSR2);
734         sleep(1);  /* don't return to the prompt too quickly... */
735       }
736       break;
737     case A_DUMP:
738       dump_notes(1);
739       break;
740     case A_DUMP_RAW:
741       dump_notes(0);
742       break;
743 #ifdef CREASES
744     case A_IRON:
745       for (i = 0; i < notes_count; memset(ndata[i++].creases, 0, 32));
746       notes_io(1);
747       if (running) {
748         kill(pid, SIGUSR2);
749         sleep(1);  /* don't return to the prompt too quickly... */
750       }
751       fprintf(stderr,
752         "Hey, ironing isn't part of my job contract, you know...\n");
753       break;
754 #endif
755     case A_DEL:
756       i = (int) strtol((const char*) data, 0, 10);
757       if (i < 0 || i >= notes_count) die("Specified note doesn't exist.");
758       remove_note(i);
759       notes_io(1);
760       if (running) {
761         kill(pid, SIGUSR2);
762         sleep(1);  /* don't return to the prompt too quickly... */
763       }
764       fprintf(stderr, "Deleted note #%d.\n", i);
765       break;
766     case A_ADD: case A_ADD_RAW:
767       if ((k = add_note()) < 0) die("Maximal number of notes reached.");
768       s = (const char*) data;
769       while (*s == '%') {
770         if (!strncmp("%%", s, 2))
771           s++;
772         else if (strlen(s) >= 2 && *s == '%') {  /* color/position code given */
773           if (isalpha(s[1])) {  /* color code? */
774             i = 0;
775             switch (toupper(s[1])) {
776               case 'G':  i = 0; break;
777               case 'Y':  i = 1; break;
778               case 'R':  i = 2; break;
779               case 'B':  i = 3; break;
780               default: die("Unknown color code.");
781             }
782             while (c_group[ndata[k].col] != i) ndata[k].col = rand() % C_NUM;
783           }
784           if (!isalpha(s[1])) {  /* position code? */
785             i = s[1] - '0';
786             if (!i--) die("Invalid position code.");
787             ndata[k].x = 6+i%3*12 + rand()%12;
788             ndata[k].y = 3+(2-i/3)*14 + rand()%14;
789           }
790           s += 2;
791         }
792       }
793       if (!strlen(s)) die("Won't add blank note.");
794       i = paste_string(k, 0, s, type == A_ADD_RAW);
795       ndata[k].cursor = i > 58 ? 58 : i;
796       notes_io(1);
797       if (running) {
798         kill(pid, SIGUSR2);
799         sleep(1);  /* don't return to the prompt too quickly... */
800       }
801       fprintf(stderr, "Added note #%d.\n", k);
802       break;
803     case A_EXPORT:
804       k = strtol((const char*) data, 0, 10);
805       if (k < 0 || k > notes_count-1) die("Specified note doesn't exist.");
806       puts("static const char sketch[512] = {");
807       for (i = 0; i < 64; i++) {
808         printf("  ");
809         for (j = 0; j < 8; j++) {
810           if (j) printf(", ");
811           printf("0x%02x", (unsigned char) ndata[k].sketch[i*8+j]);
812         }
813         if (i < 63) printf(",");
814         printf("\n");
815       }
816       puts("};");
817       break;
818     case M_INFO:
819       for (i = 0, k = 0; i < notes_count; i++)
820         if (ndata[i].a_flags & ALARM_ON) k++;
821       printf("user configuration:\n"
822         "  - font: %s\n"
823         "  - theme: %s\n"
824         "  - alarm command: %s\n",
825         strlen(opts.font) ? opts.font : fonts[0].font,
826         strlen(opts.theme) ? opts.theme : "[default]",
827         strlen(opts.alarm_cmd) ? opts.alarm_cmd : "[none]");
828       printf("\nuseless statistics:\n"
829         "  - wmpinboard has saved you (at least) %d real note%s so far\n"
830         "  - there %s currently %d note%s on your board\n"
831         "  - %d note%s\n",
832         state.counter, (state.counter != 1 ? "s" : ""),
833         (notes_count != 1 ? "are" : "is"), notes_count,
834         (notes_count != 1 ? "s" : ""), k,
835         (k != 1 ? "s have alarms set" : " has an alarm set"));
836   }
837   exit(EXIT_SUCCESS);
838 }
839 
840 /*
841  * prints a help screen and exits
842  */
843 void
help(void)844 help(void)
845 {
846   int i;
847 
848   printf("wmpinboard v" VERSION "\n\n"
849     "Copyright (C) 1998-2000 by Marco G\"otze, <mailto:gomar@mindless.com>.\n"
850     "This program is distributed under the terms of the GNU GPL2.\n\n"
851     "usage: %s [options]\n\n"
852     "configuration directives (see the documentation for detailed information):\n"
853     "           --font=FONT        use the specified font; FONT can be one of the\n"
854     "                              following:\n",
855     opts.name);
856   for (i = 0; i < NUM_FONTS; i++)
857     printf("                                %-8s  %s\n", fonts[i].name, fonts[i].desc);
858   printf(
859     "                              or a complete X descriptor of a fixed size 6x10\n"
860     "                              font\n"
861     "           --theme=FILE       use the specified theme rather than the default\n"
862     "                              board/panel images (\"default\" or zero-length\n"
863     "                              string reverts to default)\n"
864     "           --alarm-cmd=CMD    execute the specified command on alarms\n"
865     "                              (\"\" disables)\n\n"
866     "run-time options:\n"
867     "  -d DISP, --display=DISP     use the specified X display\n"
868     "  -n,      --normal-state     force NormalState (AS Wharf)   \\ mutually\n"
869     "  -w,      --withdrawn-state  force WithdrawnState (WM dock) / exclusive\n"
870     "  -t TIME, --timeout=TIME     set edit mode timeout to TIME seconds\n"
871     "                              (default %ds, 0 disables)\n"
872     "  -c,      --click-to-focus   emulate click-based keyboard focus\n"
873     "           --light            no animations\n\n"
874     "command-line actions:\n"
875     "           --dump             dump the contents of all notes\n"
876     "           --dump-raw         dump the *raw* contents of all notes\n"
877     "           --del=NUMBER       delete note NUMBER (as identified by a dump)\n"
878     "           --add=STRING       add a note with STRING as its contents, trying\n"
879     "                              to word-wrap the text\n"
880     "           --add-raw=STRING   add a note with STRING as its *raw* contents\n\n"
881     "general options:\n"
882     "  -h,      --help             print this help\n"
883     "  -i,      --info             print user configuration and statistical\n"
884     "                              information\n"
885     "  -v,      --version          print some more detailed version information\n\n"
886     "See the wmpinboard(1) man page for more information, hints, and explanations.\n"
887     "For themes and updates, check out the program's home page at\n"
888     "<http://www.tu-ilmenau.de/~gomar/stuff/wmpinboard/>.\n",
889     TIMEOUT);
890   exit(EXIT_FAILURE);
891 }
892 
893 /*
894  * parses the list of command line arguments and initializes the application's
895  * opts structure accordingly; handles non-interactive actions, and eventually
896  * reads in the data file if the instance is to be run in interactive mode
897  */
898 void
parse_argv(int argc,char ** argv)899 parse_argv(int argc, char **argv)
900 {
901   static const struct option long_opts[] = {
902     { "font", required_argument, 0, 'f' },
903     { "theme", required_argument, 0, 'p' },
904     { "alarm-cmd", required_argument, 0, 'r' },
905 
906     { "display", required_argument, 0, 'd' },
907     { "normal-state", no_argument, 0, 'n' },
908     { "withdrawn-state", no_argument, 0, 'w' },
909     { "time", required_argument, 0, 't' },
910     { "click-to-focus", no_argument, 0, 'c' },
911     { "light", no_argument, 0, 'l' },
912 
913     { "dump", no_argument, 0, 'u' },
914     { "dump-raw", no_argument, 0, 'y' },
915     { "del", required_argument, 0, 'e' },
916     { "add", required_argument, 0, 'a' },
917     { "add-raw", required_argument, 0, 'b' },
918 #ifdef CREASES
919     { "iron", no_argument, 0, 'z' },
920 #endif
921     { "export-sketch", required_argument, 0, 'x' },
922 
923     { "help", no_argument, 0, 'h' },
924     { "info", no_argument, 0, 'i' },
925     { "version", no_argument, 0, 'v' },
926     { 0, 0, 0, 0 }
927   };
928   static const char short_opts[] = "d:nwt:chiv";
929 
930   if (rindex(argv[0], '/'))
931     opts.name = (char*) rindex(argv[0], '/') + 1;
932   else
933     opts.name = argv[0];
934 
935   for(;;) {
936     int idx = 0, c;
937 
938     if ((c = getopt_long(argc, argv, short_opts, long_opts, &idx)) == -1)
939       break;
940     switch (c) {
941       case 'f': action(C_FONT, optarg);
942       case 'p': action(C_THEME, optarg);
943       case 'r': action(C_ALARM_CMD, optarg);
944       case 'd':  /* display */
945         if (opts.display) free(opts.display);
946         opts.display = smalloc(strlen(optarg)+1);
947         strcpy(opts.display, optarg);
948         break;
949       case 'n':  /* NormalState */
950         opts.window_state = NormalState;
951         break;
952       case 'w':  /* WithdrawnState */
953         opts.window_state = WithdrawnState;
954         break;
955       case 't':  /* timeout */
956         opts.timeout = strtol(optarg, 0, 10);
957         if (opts.timeout < 0) opts.timeout = -opts.timeout;
958         break;
959       case 'c':  /* click-to-focus emulation */
960         opts.click_to_focus = 1;
961         break;
962       case 'l':  /* light (no animations) */
963         opts.animate = 0;
964         break;
965       case 'u': action(A_DUMP, 0);
966       case 'y': action(A_DUMP_RAW, 0);
967       case 'e': action(A_DEL, optarg);
968       case 'a': action(A_ADD, optarg);
969       case 'b': action(A_ADD_RAW, optarg);
970 #ifdef CREASES
971       case 'z': action(A_IRON, 0);
972 #endif
973       case 'x': action(A_EXPORT, optarg);
974       case 'h': help();
975       case 'i': action(M_INFO, 0);
976       case 'v':
977         printf("wmpinboard v" VERSION "\n\ncompile-time configuration:\n"
978           "  - maximal number of notes is %d\n"
979 #if TIMEOUT == 0
980           "  - edit mode timeout is disabled by default\n"
981 #else
982           "  - default edit mode timeout is %d seconds\n"
983 #endif
984           "  - wear & tear of notes (creases) is "
985 #ifdef CREASES
986           "enabled\n"
987 #else
988           "disabled\n"
989 #endif
990 #ifndef FUNSTUFF
991           "  - FUNSTUFF is disabled  :-/\n"
992 #endif
993           , MAX_NOTES
994 #if TIMEOUT != 0
995           , TIMEOUT
996 #endif
997           );
998         exit(EXIT_SUCCESS);
999       default : exit(EXIT_FAILURE);
1000     }
1001   }
1002   if (optind < argc) help();  /* exits */
1003   if (flush_instance(notes_io(0)))
1004     die("Another interactive instance is running.");
1005 }
1006 
1007 /*
1008  * draws a text cursor at character <pos> (block cursor if <block> is true)
1009  * _without_ updating the display
1010  */
1011 void
draw_cursor(int pos,int block)1012 draw_cursor(int pos, int block)
1013 {
1014   unsigned long c = palette[ndata[state.cur_note].col].fg;
1015   unsigned long d = palette[ndata[state.cur_note].col].bg;
1016   int i, j;
1017 
1018   XGetSubImage(display, app, 2+6*(pos%10), 2+10*(pos/10), 6, 10, ~0, ZPixmap,
1019     img, 64, 54);
1020   for (i = 64; i < 70; i++)
1021     for (j = 63; j > (block ? 53 : 62); j--)
1022       XPutPixel(img, i, j, XGetPixel(img, i, j) == c ? d : c);
1023   XPutImage(display, app, normalGC, img, 64, 54, 2+6*(pos%10), 2+10*(pos/10),
1024     6, 10);
1025 }
1026 
1027 /*
1028  * in edit mode, moves the cursor to a new position and updates the display
1029  * if <update> has a true value
1030  */
1031 void
set_cursor(int new_pos,int update)1032 set_cursor(int new_pos, int update)
1033 {
1034   int in_sel = 0;
1035 
1036   if (new_pos > 58) return;
1037   in_sel = (state.sel_from >= 0 && state.sel_to >= 0 &&
1038     ((state.sel_from <= new_pos && new_pos < state.sel_to) ||
1039     (state.sel_to <= new_pos && new_pos < state.sel_from)));
1040   if (!in_sel || state.insert)
1041     print_letter(state.cur_note, ndata[state.cur_note].cursor, 1);
1042   draw_cursor(ndata[state.cur_note].cursor = new_pos, !in_sel && state.insert);
1043   if (update) redraw_window();
1044 }
1045 
1046 /*
1047  * selects text in the character range state.sel_from..<to>, previously
1048  * unselecting that between state.sel_from..state.sel_to
1049  */
1050 void
sel_text(int to)1051 sel_text(int to)
1052 {
1053   int i, t;
1054 
1055   if (to == state.sel_to) return;
1056   if (state.sel_to >= 0) {
1057     i = state.sel_from < state.sel_to ? state.sel_from : state.sel_to;
1058     t = state.sel_from < state.sel_to ? state.sel_to : state.sel_from;
1059     for (; i < t; i++) print_letter(state.cur_note, i, 1);
1060   }
1061   print_letter(state.cur_note, ndata[state.cur_note].cursor, 1);
1062   i = state.sel_from < to ? state.sel_from : to;
1063   t = state.sel_from < to ? to : state.sel_from;
1064   for (; i < t; i++) draw_cursor(i, 1);
1065   state.sel_to = to;
1066   set_cursor(ndata[state.cur_note].cursor, 1);
1067 }
1068 
1069 /*
1070  * clears the selection if in edit mode and text is selected
1071  */
1072 void
clear_selection()1073 clear_selection()
1074 {
1075   if (state.mode == M_EDIT && state.sel_from >= 0) {
1076     state.sel_from = state.sel_to = -1;
1077     init_edit_mode(state.cur_note);
1078     set_cursor(ndata[state.cur_note].cursor, 1);
1079   }
1080 }
1081 
1082 /*
1083  * in sketch mode, draws a pixel at (x, y)
1084  */
1085 void
draw_pixel(int x,int y)1086 draw_pixel(int x, int y)
1087 {
1088   if (!x || !y || x > 62 || y > 62 || x+y >= 115) return;
1089   XDrawPoint(display, win, state.sketchGC, x, y);  /* actual drawing */
1090   if (state.mode == M_DRAW)  /* save bits */
1091     ndata[state.cur_note].sketch[x/8 + y*8] |= 1<<(x%8);
1092   else
1093     ndata[state.cur_note].sketch[x/8 + y*8] &= ~(1<<(x%8));
1094 }
1095 
1096 /*
1097  * sets the internal mode indicator and installs a corresponding mouse cursor
1098  */
1099 void
set_mode(modes new)1100 set_mode(modes new)
1101 {
1102   switch (state.mode = new) {
1103     case M_MOVE:
1104       XDefineCursor(display, win, cursors[state.cur_note >= 0 ? 0 : 1]);
1105       break;
1106     case M_DRAW: case M_ERAS:
1107       XDefineCursor(display, win, cursors[2]);
1108       break;
1109     case M_EDIT:
1110       state.selecting = 0;
1111       state.sel_from = state.sel_to = -1;
1112       if (!opts.click_to_focus) set_kbfocus(1);
1113     default:
1114       XUndefineCursor(display, win);
1115   }
1116 }
1117 
1118 /*
1119  * returns from M_EDIT, M_DRAW, M_ERAS, or M_BBAR to M_NOOP; destroys the
1120  * current note if empty or <destroy> is true
1121  */
1122 void
quit_edit_mode(int destroy,int save)1123 quit_edit_mode(int destroy, int save)
1124 {
1125   if (state.mode == M_BBAR) {
1126     if (ndata[state.cur_note].a_flags & ALARM_ON) animate_abar(0);
1127     animate_bbar(0);
1128   }
1129   if (destroy || (state.cur_note >= 0 && note_empty(state.cur_note))) {
1130     remove_note(state.cur_note);
1131     destroy = 1;
1132   }
1133   animate_note(destroy ? 6 : 5);
1134   set_mode(M_NOOP);
1135   if (save) notes_io(1);  /* should be last when called from signal handler */
1136   time_next_alarm();
1137 }
1138 
1139 /*
1140  * sets a timer expiring every <intv> microseconds
1141  */
1142 void
timer(unsigned int intv)1143 timer(unsigned int intv)
1144 {
1145   ualarm(intv, intv);
1146 }
1147 
1148 /*
1149  * adds some eyecandy to the popping-up of the button bar (slides in if <in>
1150  * is true, otherwise, out)
1151  */
1152 void
animate_bbar(int in)1153 animate_bbar(int in)
1154 {
1155   int y;
1156 
1157   init_edit_mode(state.cur_note);
1158   set_cursor(ndata[state.cur_note].cursor, 0);
1159   if (in) {  /* slide in */
1160     if (opts.animate) {
1161       redraw_window();
1162       timer(PANEL_ANI_INT);
1163       for (y = 27; y >= 0; y -= 3) {
1164         state.alarmed = 0;
1165         XCopyArea(display, bbar, win, normalGC, 0, 0, 58, 30-y, 3, 34+y);
1166         flush_expose();
1167         while (!state.alarmed);
1168       }
1169       alarm(0);
1170       XCopyArea(display, bbar, win, normalGC, 0, 0, 58, 30, 3, 31);
1171       XCopyArea(display, app, win, normalGC, 3, 61, 58, 3, 3, 61);
1172       flush_expose();
1173       /* for future refreshes... */
1174       XCopyArea(display, bbar, app, normalGC, 0, 0, 58, 30, 3, 31);
1175     } else {  /* no animation */
1176       XCopyArea(display, bbar, app, normalGC, 0, 0, 58, 30, 3, 31);
1177       redraw_window();
1178     }
1179   } else {  /* slide out */
1180     if (opts.animate) {
1181       timer(PANEL_ANI_INT);
1182       for (y = 31; y <= 58; y += 3) {
1183         state.alarmed = 0;
1184         XCopyArea(display, app, win, normalGC, 3, y, 58, 3, 3, y);
1185         XCopyArea(display, bbar, win, normalGC, 0, 0, 58, 61-y, 3, y+3);
1186         flush_expose();
1187         while (!state.alarmed);
1188       }
1189       alarm(0);
1190       XCopyArea(display, app, win, normalGC, 3, 61, 58, 3, 3, 61);
1191       flush_expose();
1192     } else  /* no animation */
1193       redraw_window();
1194   }
1195 }
1196 
1197 /*
1198  * like animate_bbar, but for abar (to be called when bbar is already visible)
1199  */
1200 void
animate_abar(int in)1201 animate_abar(int in)
1202 {
1203   int y;
1204 
1205   if (in) {  /* slide in */
1206     explode_time(state.cur_note);
1207     render_abar(state.cur_note);
1208     if (opts.animate) {
1209       redraw_window();
1210       timer(PANEL_ANI_INT);
1211       for (y = 28; y >= 3; y -= 3) {
1212         state.alarmed = 0;
1213         XCopyArea(display, abar, win, normalGC, 0, 0, 58, 31-y, 3, y);
1214         flush_expose();
1215         while (!state.alarmed);
1216       }
1217       alarm(0);
1218       XCopyArea(display, abar, win, normalGC, 0, 0, 58, 28, 3, 4);
1219       flush_expose();
1220       /* for future refreshes... */
1221       XCopyArea(display, abar, app, normalGC, 0, 0, 58, 28, 3, 4);
1222     } else {  /* no animation */
1223       XCopyArea(display, abar, app, normalGC, 0, 0, 58, 28, 3, 4);
1224       redraw_window();
1225     }
1226   } else {  /* slide out */
1227     implode_time(state.cur_note);
1228     /* render app as note plus bbar */
1229     init_edit_mode(state.cur_note);
1230     set_cursor(ndata[state.cur_note].cursor, 0);
1231     XCopyArea(display, win, app, normalGC, 3, 31, 58, 30, 3, 31);
1232     if (opts.animate) {
1233       timer(PANEL_ANI_INT);
1234       for (y = 4; y <= 25; y += 3) {
1235         state.alarmed = 0;
1236         XCopyArea(display, app, win, normalGC, 3, y, 58, 3, 3, y);
1237         XCopyArea(display, abar, win, normalGC, 0, 0, 58, 28-y, 3, y+3);
1238         flush_expose();
1239         while (!state.alarmed);
1240       }
1241       alarm(0);
1242       XCopyArea(display, app, win, normalGC, 3, 28, 58, 3, 3, 28);
1243       flush_expose();
1244     } else  /* no animation */
1245       redraw_window();
1246   }
1247 }
1248 
1249 /*
1250  * animates the switching between two notes (replaces what's currently
1251  * being displayed by state.cur_note), in a way specified by <style>
1252  *
1253  *   0 = right -> left   2 = bottom -> top   4 = pinboard -> edit mode
1254  *   1 = left -> right   3 = top -> bottom   5 = edit mode -> pinboard
1255  *                                           6 = note destruction
1256  */
1257 void
animate_note(int style)1258 animate_note(int style)
1259 {
1260   static const int seq[10] = { 2, 3, 6, 9, 12, 12, 9, 6, 3, 2 };
1261   XRectangle mask[5] = { { 6, 2, 52, 60 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 },
1262     { 0, 0, 0, 0 }, { 0, 0, 0, 0 } };
1263   int i, j;
1264 
1265   if (!opts.animate) {  /* animations disabled */
1266     if (style < 5) {  /* display note */
1267       set_mask(0);
1268       init_edit_mode(state.cur_note);
1269       set_cursor(ndata[state.cur_note].cursor, 1);
1270     } else {  /* display pinboard */
1271       set_mask(1);
1272       render_pinboard(-1);
1273       redraw_window();
1274     }
1275     return;
1276   }
1277   switch (style) {
1278     case 0:  /* slide right -> left */
1279       init_edit_mode(state.cur_note);
1280       set_cursor(ndata[state.cur_note].cursor, 0);
1281       timer(NOTE_ANI_INT);
1282       for (i = 2, j = 0; j < 10; i += seq[++j]) {
1283         state.alarmed = 0;
1284         XCopyArea(display, win, win, normalGC, seq[j], 0, 64-seq[j], 64, 0, 0);
1285         XCopyArea(display, app, win, normalGC, i-seq[j], 0, seq[j], 64,
1286           64-seq[j], 0);
1287         flush_expose();
1288         while (!state.alarmed);
1289       }
1290       break;
1291     case 1:  /* slide left -> right */
1292       init_edit_mode(state.cur_note);
1293       set_cursor(ndata[state.cur_note].cursor, 0);
1294       timer(NOTE_ANI_INT);
1295       for (i = 2, j = 0; j < 10; i += seq[++j]) {
1296         state.alarmed = 0;
1297         XCopyArea(display, win, win, normalGC, 0, 0, 64-seq[j], 64, seq[j], 0);
1298         XCopyArea(display, app, win, normalGC, 64-i, 0, seq[j], 64, 0, 0);
1299         flush_expose();
1300         while (!state.alarmed);
1301       }
1302       break;
1303     case 2:  /* slide top -> bottom */
1304       init_edit_mode(state.cur_note);
1305       set_cursor(ndata[state.cur_note].cursor, 0);
1306       timer(NOTE_ANI_INT);
1307       for (i = 2, j = 0; j < 10; i += seq[++j]) {
1308         state.alarmed = 0;
1309         XCopyArea(display, win, win, normalGC, 0, 0, 64, 64-seq[j], 0, seq[j]);
1310         XCopyArea(display, app, win, normalGC, 0, 64-i, 64, seq[j], 0, 0);
1311         flush_expose();
1312         while (!state.alarmed);
1313       }
1314       break;
1315     case 3:  /* slide bottom -> top */
1316       init_edit_mode(state.cur_note);
1317       set_cursor(ndata[state.cur_note].cursor, 0);
1318       timer(NOTE_ANI_INT);
1319       for (i = 2, j = 0; j < 10; i += seq[++j]) {
1320         state.alarmed = 0;
1321         XCopyArea(display, win, win, normalGC, 0, seq[j], 64, 64-seq[j], 0, 0);
1322         XCopyArea(display, app, win, normalGC, 0, i-seq[j], 64, seq[j], 0,
1323           64-seq[j]);
1324         flush_expose();
1325         while (!state.alarmed);
1326       }
1327       break;
1328     case 4:  /* pinboard view -> edit mode */
1329       mask[1].x = mask[1].y = 0;
1330       init_edit_mode(state.cur_note);
1331       set_cursor(ndata[state.cur_note].cursor, 0);
1332       timer(NOTE_ANI_INT);
1333       for (i = 2, j = 0; j < 10; i += seq[++j]) {
1334         state.alarmed = 0;
1335         mask[1].width = mask[1].height = i;
1336         XShapeCombineRectangles(display, win, ShapeBounding, 0, 0, mask, 2,
1337           ShapeSet, 0);
1338         XCopyArea(display, app, win, normalGC, 64-i, 64-i, i, i, 0, 0);
1339         flush_expose();
1340         while (!state.alarmed);
1341       }
1342       break;
1343     case 5:  /* edit mode -> pinboard view */
1344       render_pinboard(-1);
1345       timer(NOTE_ANI_INT);
1346       for (i = 2, j = 0; j < 10; i += seq[++j]) {
1347         state.alarmed = 0;
1348         mask[1].x = mask[1].y = i;
1349         mask[1].width = mask[1].height = 64-i;
1350         XCopyArea(display, win, win, normalGC, i-seq[j], i-seq[j], 64-i, 64-i,
1351           i, i);
1352         XCopyArea(display, app, win, normalGC, i-seq[j], i-seq[j], seq[j], 64,
1353           i-seq[j], i-seq[j]);
1354         XCopyArea(display, app, win, normalGC, i, i-seq[j], 64-i, seq[j],
1355           i, i-seq[j]);
1356         XShapeCombineRectangles(display, win, ShapeBounding, 0, 0, mask, 2,
1357           ShapeSet, 0);
1358         flush_expose();
1359         while (!state.alarmed);
1360       }
1361       break;
1362     case 6:  /* note destruction */
1363       render_pinboard(-1);
1364       timer((int) 1.5*NOTE_ANI_INT);
1365       /* mask[1].x = mask[1].y = mask[2].y = mask[3].x = 0; */
1366       for (i = 4; i <= 32; i += 4) {
1367         state.alarmed = 0;
1368         for (j = 1; j < 5; j++) mask[j].width = mask[j].height = 32-i;
1369         mask[2].x = mask[3].y = mask[4].x = mask[4].y = 32+i;
1370         XCopyArea(display, win, win, normalGC, 4, 4, 32-i, 32-i, 0, 0);
1371         XCopyArea(display, win, win, normalGC, 28+i, 4, 32-i, 32-i, 32+i, 0);
1372         XCopyArea(display, win, win, normalGC, 4, 28+i, 32-i, 32-i, 0, 32+i);
1373         XCopyArea(display, win, win, normalGC, 28+i , 28+i, 32-i, 32-i, 32+i,
1374           32+i);
1375         XCopyArea(display, app, win, normalGC, 32-i, 0, 2*i, 64, 32-i, 0);
1376         XCopyArea(display, app, win, normalGC, 0, 32-i, 64, 2*i, 0, 32-i);
1377         XShapeCombineRectangles(display, win, ShapeBounding, 0, 0, mask, 5,
1378           ShapeSet, 0);
1379         flush_expose();
1380         while (!state.alarmed);
1381       }
1382   }
1383   alarm(0);
1384 }
1385 
1386 /*
1387  * displays the next phase of the alarm animation (flashing)
1388  */
1389 void
animate_alarm()1390 animate_alarm()
1391 {
1392   XCopyArea(display, state.alarm.buffer, win, normalGC,
1393     state.alarm.phase*64, 0, 64, 64, 0, 0);
1394   flush_expose();
1395   state.alarm.phase = !state.alarm.phase;
1396 }
1397 
1398 /*
1399  * called from main event loop whenever a ButtonPress event occurs
1400  */
1401 void
handle_ButtonPress(XEvent * event)1402 handle_ButtonPress(XEvent *event)
1403 {
1404   int i;
1405 
1406   if (state.button) return;
1407   state.button = event->xbutton.button;
1408   switch (state.mode) {
1409     case M_NOOP:  /* drag a note? */
1410       state.cur_note = selected_note(event->xbutton.x, event->xbutton.y);
1411       if (state.cur_note >= 0) {  /* drag existing note */
1412         state.dx = event->xbutton.x - ndata[state.cur_note].x;
1413         state.dy = event->xbutton.y - ndata[state.cur_note].y;
1414         state.moved = 0;
1415         render_pinboard(state.cur_note);
1416         XCopyArea(display, app, app, normalGC,
1417           ndata[state.cur_note].x, ndata[state.cur_note].y, 16, 16, 64, 48);
1418         color_notes(state.cur_note);
1419         pin_note(state.cur_note);
1420         redraw_window();  /* necessary in case of a single raising click */
1421         state.mode = M_MOVE;  /* don't set drag cursor immediately */
1422       } else if (
1423         event->xbutton.x >= 6+label_coords[0] &&
1424         event->xbutton.x <= 6+label_coords[2] &&
1425         event->xbutton.y >= 2+label_coords[1] &&
1426         event->xbutton.y <= 2+label_coords[3] &&
1427         selected_note(event->xbutton.x, event->xbutton.y) < 0 &&
1428         notes_count < MAX_NOTES-1)
1429       {  /* possibly drag new note from "TO DO" label */
1430         state.moved = 0;
1431         set_mode(M_MOVE);
1432       }
1433       break;
1434     case M_EDIT:  /* claim focus in click-based focus emulation mode */
1435       if (!opts.click_to_focus || state.clicks_count) {
1436         if (state.button != 2) {
1437           if (state.lp_btn == state.button &&
1438             event->xbutton.time-state.lp_time < DCLICK_LIMIT)
1439           {
1440             state.sel_from = i = char_at(event->xbutton.x, event->xbutton.y, 0);
1441             for (; state.sel_from &&
1442               ndata[state.cur_note].text[state.sel_from-1] != ' ' &&
1443               (state.button != 1 || state.sel_from%10 > 0); state.sel_from--);
1444             for (; i < 59 && ndata[state.cur_note].text[i] != ' ' &&
1445               (state.button != 1 || state.sel_from/10 == (i+1)/10 || i%10);
1446               i++);
1447             sel_text(i);
1448             cb_copy(&ndata[state.cur_note].text[state.sel_from], i -
1449               state.sel_from);
1450           } else {
1451             clear_selection();
1452             state.sel_from = char_at(event->xbutton.x, event->xbutton.y, 0);
1453             state.selecting = 1;
1454           }
1455         }
1456       }
1457       if (opts.click_to_focus && event->xbutton.x+event->xbutton.y < 115) {
1458         if (!state.clicks_count) set_kbfocus(1);
1459         state.clicks_count++;
1460       }
1461       break;
1462     case M_BBAR:  /* remember which button was *pressed* */
1463       state.bbar_pressed = state.abar_pressed = -1;
1464       /* button pressed on bbar? */
1465       i = bbar_button(event->xbutton.x, event->xbutton.y);
1466       if (i >= 0) {  /* bbar button */
1467         state.bbar_pressed = i;
1468       } else if (ndata[state.cur_note].a_flags & ALARM_ON) {  /* abar? */
1469         i = abar_area(event->xbutton.x, event->xbutton.y);
1470 		if (i >= 0) state.abar_pressed = i;
1471       }
1472       break;
1473     case M_DRAW: case M_ERAS:  /* draw in either sketch mode */
1474       if (state.button == 1) {
1475         draw_pixel(event->xbutton.x, event->xbutton.y);
1476 #ifdef CREASES
1477         if (state.mode == M_ERAS) {
1478           render_edit_wear_area_win(win, state.cur_note, event->xbutton.x,
1479             event->xbutton.y, 1, 1);
1480         }
1481 #endif
1482       }
1483       break;
1484     default:  /* keep the compiler happy */
1485       break;
1486   }
1487   state.lp_btn = state.button;
1488   state.lp_time = event->xbutton.time;
1489 }
1490 
1491 /*
1492  * called from main event loop whenever a ButtonRelease event occurs
1493  */
1494 void
handle_ButtonRelease(XEvent * event)1495 handle_ButtonRelease(XEvent *event)
1496 {
1497   XGCValues gcv;
1498   char *s;
1499   int i;
1500 
1501   if (event->xbutton.button != state.button) return;
1502   switch (state.mode) {
1503     case M_NOOP: case M_MOVE:  /* add new note or edit existing one? */
1504       if (!state.moved && state.button == 1) {  /* left-click */
1505         if (
1506           event->xbutton.x >= 6+label_coords[0] &&
1507           event->xbutton.x <= 6+label_coords[2] &&
1508           event->xbutton.y >= 2+label_coords[1] &&
1509           event->xbutton.y <= 2+label_coords[3] &&
1510           selected_note(event->xbutton.x, event->xbutton.y) < 0)
1511         { /* add new note */
1512           if ((state.cur_note = add_note()) >= 0) {
1513             animate_note(4);
1514             set_kbfocus(1);
1515             set_mode(M_EDIT);
1516           } else  /* beep if maximum number of notes reached */
1517             XBell(display, 0);
1518         } else if (state.cur_note >= 0 && state.cur_note ==
1519           selected_note(event->xbutton.x, event->xbutton.y))
1520         { /* open existing note in edit mode */
1521 #ifdef CREASES
1522           wear_note(state.cur_note);
1523 #endif
1524           animate_note(4);
1525           state.clicks_count = 0;
1526           set_mode(M_EDIT);
1527         }
1528       } else {  /* moved */
1529         if (state.cur_note >= 0) {
1530           if (string_empty(ndata[state.cur_note].text, 0)) {  /* new note */
1531             animate_note(4);
1532             set_kbfocus(1);
1533             set_mode(M_EDIT);
1534           } else {  /* existing note dragged via either left or right MB */
1535 #ifdef CREASES
1536             wear_note(state.cur_note);
1537 #endif
1538             notes_io(1);
1539             if (state.button == 1) {  /* keep level */
1540               render_pinboard(-1);
1541               redraw_window();
1542             } else {  /* raise */
1543               state.cur_note = raise_note(state.cur_note);
1544 #ifdef CREASES
1545               pin_note(state.cur_note);
1546               redraw_window();
1547 #endif
1548             }
1549             time_next_alarm();  /* note IDs may have changed */
1550           }
1551         }
1552       }
1553       if (state.mode != M_EDIT) set_mode(M_NOOP);
1554       break;
1555     case M_EDIT: case M_DRAW: case M_ERAS:
1556       i = char_at(event->xbutton.x, event->xbutton.y, 0);
1557       if (event->xbutton.x + event->xbutton.y >= 115 &&
1558         (!state.selecting || state.sel_from == i))
1559       {  /* clicked triangle? */
1560         if (state.button > 1) {  /* open panel */
1561           animate_bbar(1);
1562           if (ndata[state.cur_note].a_flags & ALARM_ON) {
1563             animate_abar(1);
1564             check_time(state.cur_note);  /* adjusts hidden year field */
1565           }
1566           if (!opts.click_to_focus) set_kbfocus(0);
1567           set_mode(M_BBAR);
1568         } else {  /* end edit mode */
1569           if (state.mode == M_DRAW || state.mode == M_ERAS) {
1570             XFreeGC(display, state.sketchGC);  /* free temporary GC */
1571             if (M_ERAS) {  /* restore normal note display first */
1572               print_text(state.cur_note);
1573               draw_sketch(state.cur_note);
1574               set_cursor(ndata[state.cur_note].cursor, 1);
1575             }
1576             set_mode(M_EDIT);
1577           }
1578           quit_edit_mode(0, 1);
1579           if (!opts.click_to_focus) set_kbfocus(0);
1580         }
1581       } else {
1582         if (state.mode == M_EDIT) {
1583           if (!opts.click_to_focus || state.clicks_count > 1) {
1584             if (state.button == 2) {
1585               ndata[state.cur_note].cursor = char_at(event->xbutton.x,
1586                 event->xbutton.y, 1);
1587               state.raw_paste = 0;
1588               cb_paste(state.cur_note, state.insert);
1589               init_edit_mode(state.cur_note);
1590               set_cursor(ndata[state.cur_note].cursor, 1);
1591             } else if (state.selecting && state.sel_from != i) {
1592               sel_text(i);
1593               if (state.sel_from > state.sel_to) {
1594                 i = state.sel_from;
1595                 state.sel_from = state.sel_to;
1596                 state.sel_to = i;
1597               }
1598               if (state.button == 1) {  /* copy cooked */
1599                 s = cook(state.cur_note, state.sel_from, state.sel_to -
1600                   state.sel_from);
1601                 cb_copy(s, -1);
1602                 free(s);
1603               } else {  /* copy raw */
1604                 cb_copy(&ndata[state.cur_note].text[state.sel_from],
1605                   state.sel_to - state.sel_from);
1606               }
1607             } else
1608               set_cursor(char_at(event->xbutton.x, event->xbutton.y, 1), 1);
1609           }
1610         } else if (state.button > 1) {  /* revert from sketch to edit mode */
1611           XFreeGC(display, state.sketchGC);
1612           init_edit_mode(state.cur_note);
1613           set_cursor(ndata[state.cur_note].cursor, 1);
1614           set_mode(M_EDIT);
1615         }
1616       }
1617       state.selecting = 0;
1618       break;
1619     case M_BBAR:  /* actions in panel mode */
1620       if (state.bbar_pressed >= 0) {
1621         if (state.bbar_pressed == bbar_button(event->xbutton.x,
1622           event->xbutton.y))  /* clicked on panel? */
1623         {
1624           switch (state.bbar_pressed) {
1625             case 0:  /* open/close alarm panel */
1626               if (!(ndata[state.cur_note].a_flags & ALARM_ON)) {
1627                 /* open alarm panel, turn alarm on */
1628                 animate_abar(1);
1629                 ndata[state.cur_note].a_flags |= ALARM_ON;
1630               } else {  /* close alarm panel, turn alarm off */
1631                 animate_abar(0);
1632                 ndata[state.cur_note].a_flags &= ~ALARM_ON;
1633               }
1634               break;
1635             case 4:  /* change note color */
1636               i = ndata[state.cur_note].col;
1637               if (state.button > 1) {  /* previous color */
1638                 if (--ndata[state.cur_note].col < 0)
1639                   ndata[state.cur_note].col = C_NUM-1;
1640               } else  /* next color */
1641                 if (++ndata[state.cur_note].col == C_NUM)
1642                   ndata[state.cur_note].col = 0;
1643               init_edit_mode(state.cur_note);
1644               set_cursor(ndata[state.cur_note].cursor, 0);
1645               XCopyArea(display, bbar, app, normalGC, 0, 0, 58, 30, 3, 31);
1646               if (ndata[state.cur_note].a_flags & ALARM_ON)
1647                 XCopyArea(display, abar, app, normalGC, 0, 0, 58, 28, 3, 4);
1648               redraw_window();
1649               break;
1650             case 1: case 5:  /* enter draw/erase mode */
1651               gcv.foreground = state.bbar_pressed == 1 ?
1652                 palette[ndata[state.cur_note].col].fg :
1653                 palette[ndata[state.cur_note].col].bg;
1654               state.sketchGC = XCreateGC(display, app, GCForeground, &gcv);
1655               if (ndata[state.cur_note].a_flags & ALARM_ON) animate_abar(0);
1656               animate_bbar(0);
1657               render_note(state.cur_note);
1658 #ifdef CREASES
1659               render_edit_wear(state.cur_note);
1660 #endif
1661               if (state.bbar_pressed == 1) {  /* erase: hide text */
1662                 print_text(state.cur_note);
1663                 set_cursor(ndata[state.cur_note].cursor, 0);
1664               }
1665               draw_sketch(state.cur_note);
1666               redraw_window();
1667               set_mode(state.bbar_pressed == 1 ? M_DRAW : M_ERAS);
1668               break;
1669             case 2:  /* clear note's text */
1670               memset(ndata[state.cur_note].text, 32, 59);
1671 #ifdef CREASES
1672               wear_note(state.cur_note);
1673               wear_note(state.cur_note);
1674 #endif
1675               if (ndata[state.cur_note].a_flags & ALARM_ON) animate_abar(0);
1676               animate_bbar(0);
1677               init_edit_mode(state.cur_note);
1678               set_cursor(0, 1);
1679               set_mode(M_EDIT);
1680               break;
1681             case 6:  /* clear sketch */
1682               memset(ndata[state.cur_note].sketch, 0, 511);  /* not last byte */
1683 #ifdef CREASES
1684               wear_note(state.cur_note);
1685               wear_note(state.cur_note);
1686 #endif
1687               if (ndata[state.cur_note].a_flags & ALARM_ON) animate_abar(0);
1688               animate_bbar(0);
1689               init_edit_mode(state.cur_note);
1690               set_cursor(ndata[state.cur_note].cursor, 1);
1691               set_mode(M_EDIT);
1692               break;
1693             case 3:  /* remove note */
1694               quit_edit_mode(1, 1);
1695               if (!opts.click_to_focus) set_kbfocus(0);
1696               break;
1697             case 7:  /* close button bar */
1698               if (ndata[state.cur_note].a_flags & ALARM_ON) animate_abar(0);
1699               animate_bbar(0);
1700               set_mode(M_EDIT);
1701           }
1702         }
1703       } else if (state.abar_pressed >= 0) {
1704         if (state.abar_pressed == abar_area(event->xbutton.x,
1705           event->xbutton.y))  /* clicked on panel? */
1706         {
1707           if (state.abar_pressed < 4) {  /* clicked on number */
1708             char c = state.a_edit[state.abar_pressed];
1709             char delta = state.button == 1 ? 1 : -1;
1710 
1711             switch (state.abar_pressed) {
1712               case 0:  c = (24+c+delta)%24;  break;
1713               case 1:  c = (60+c+delta)%60;  break;
1714               case 2:  c = (11+c+delta)%12+1;  break;
1715               case 3:  c = (30+c+delta)%31+1;
1716             }
1717             state.a_edit[state.abar_pressed] = c;
1718             /* check validity of date, adapt month or day if necessary */
1719             while (!check_time())
1720               if (state.abar_pressed == 2) {  /* month was changed */
1721                 if (state.a_edit[3] > 1)
1722                   state.a_edit[3]--;
1723                 else
1724                   break;  /* just making sure */
1725               } else {  /* presumably, the day was changed */
1726                 state.a_edit[3] = 1;
1727                 break;
1728               }
1729             /* update display */
1730             render_abar_number(state.abar_pressed);
1731             /* always update day in case the date had to be corrected */
1732             if (state.abar_pressed != 3) render_abar_number(3);
1733           } else {  /* clicked on switch */
1734             if (state.abar_pressed == 4)  /* daily alarm */
1735               ndata[state.cur_note].a_flags &= ~ALARM_DATE;
1736             else  /* specific date */
1737               ndata[state.cur_note].a_flags |= ALARM_DATE;
1738             check_time(state.cur_note);  /* updates the hidden year field */
1739             render_abar_switches(state.cur_note);
1740           }
1741           /* update display */
1742           XCopyArea(display, abar, app, normalGC, 0, 0, 58, 28, 3, 4);
1743           XCopyArea(display, abar, win, normalGC, 0, 0, 58, 28, 3, 4);
1744         }
1745       }
1746       break;
1747     case M_ALRM:
1748       if (ndata[state.cur_note].a_flags & ALARM_DATE)
1749         ndata[state.cur_note].a_flags &= ~ALARM_ON;
1750       notes_io(1);
1751       init_edit_mode(state.cur_note);
1752       set_cursor(ndata[state.cur_note].cursor, 1);
1753       redraw_window();
1754       set_mode(M_EDIT);
1755       time(&state.idle);
1756   }
1757   state.button = 0;
1758 }
1759 
1760 /*
1761  * called from main event loop whenever a MotionNotify event occurs
1762  */
1763 void
handle_MotionNotify(XEvent * event)1764 handle_MotionNotify(XEvent *event)
1765 {
1766   int i, j;
1767 
1768   switch (state.mode) {
1769     case M_MOVE:  /* note's being dragged, update position */
1770       if (state.cur_note >= 0) {  /* update note that's being moved's pos. */
1771         /* the drag cursor isn't set immediately... */
1772         if (!state.moved) {
1773           set_mode(M_MOVE);
1774           state.moved = 1;
1775         }
1776         i = ndata[state.cur_note].x;
1777         j = ndata[state.cur_note].y;
1778         ndata[state.cur_note].x = event->xbutton.x - state.dx;
1779         ndata[state.cur_note].x = ndata[state.cur_note].x < 6 ? 6 :
1780           ndata[state.cur_note].x;
1781         ndata[state.cur_note].x = ndata[state.cur_note].x > 42 ? 42 :
1782           ndata[state.cur_note].x;
1783         ndata[state.cur_note].y = event->xbutton.y - state.dy;
1784         ndata[state.cur_note].y = ndata[state.cur_note].y < 2 ? 2 :
1785           ndata[state.cur_note].y;
1786         ndata[state.cur_note].y = ndata[state.cur_note].y > 46 ? 46 :
1787           ndata[state.cur_note].y;
1788         XCopyArea(display, app, app, normalGC, 64, 48, 16, 16, i, j);
1789         XCopyArea(display, app, app, normalGC, ndata[state.cur_note].x,
1790           ndata[state.cur_note].y, 16, 16, 64, 48);
1791         pin_note(state.cur_note);
1792         redraw_window();
1793         time(&state.idle);
1794       } else {  /* create note by dragging it "off" the "TO DO" label */
1795         state.cur_note = add_note();
1796         ndata[state.cur_note].x = event->xbutton.x-8;
1797         ndata[state.cur_note].y = event->xbutton.y < 8 ? 0 :
1798           event->xbutton.y-8;
1799         state.dx = 8;
1800         state.dy = event->xbutton.y - ndata[state.cur_note].y;
1801         state.moved = 0;
1802         XCopyArea(display, app, app, normalGC,
1803           ndata[state.cur_note].x, ndata[state.cur_note].y, 16, 16, 64, 48);
1804         color_notes(state.cur_note);
1805         set_mode(M_MOVE);  /* update cursor */
1806         pin_note(state.cur_note);
1807         redraw_window();
1808         time(&state.idle);
1809       }
1810       break;
1811     case M_DRAW: case M_ERAS:  /* draw in either sketch mode */
1812       if (state.button == 1) {
1813         draw_pixel(event->xbutton.x, event->xbutton.y);
1814         if (state.mode == M_ERAS) {  /* enlarge the cursor a bit when erasing */
1815           draw_pixel(event->xbutton.x-1, event->xbutton.y);
1816           draw_pixel(event->xbutton.x+1, event->xbutton.y);
1817           draw_pixel(event->xbutton.x, event->xbutton.y-1);
1818           draw_pixel(event->xbutton.x, event->xbutton.y+1);
1819 #ifdef CREASES
1820           render_edit_wear_area_win(win, state.cur_note, event->xbutton.x-1,
1821             event->xbutton.y, 3, 1);
1822           render_edit_wear_area_win(win, state.cur_note, event->xbutton.x,
1823             event->xbutton.y-1, 1, 3);
1824 #endif
1825         }
1826         time(&state.idle);
1827       }
1828       break;
1829     case M_EDIT:
1830       if (state.selecting) {
1831         sel_text(char_at(event->xbutton.x, event->xbutton.y, 0));
1832         time(&state.idle);
1833       }
1834       break;
1835     default:  /* keep the compilter happy */
1836       break;
1837   }
1838 }
1839 
1840 /*
1841  * called from main event loop whenever a KeyPress event occurs
1842  */
1843 void
handle_KeyPress(XEvent * event)1844 handle_KeyPress(XEvent *event)
1845 {
1846   KeySym ksym;
1847   unsigned char ch[4];
1848   char *s;
1849   int i, j = 0;
1850 
1851   if (state.mode != M_EDIT || event->xkey.state & 0xdfe8 ||
1852     (InputContext && XFilterEvent(event, win)))
1853   {
1854     XSendEvent(display, RootWindow(display, DefaultScreen(display)), True,
1855       KeyPressMask, event);
1856     if (state.mode == M_EDIT) set_kbfocus(1);  /* make sure we keep focus */
1857     return;
1858   }
1859   time(&state.idle);
1860 
1861   clear_selection();
1862   if (event->xkey.state & ControlMask) { /* [Ctrl]-anything -> special fn.s */
1863     /* InputContext intentionally ignored here... */
1864     XLookupString(&event->xkey, (char*) ch, sizeof(ch), &ksym, &state.compose);
1865     switch (ksym) {
1866       case XK_c: case XK_C:  /* copy cooked */
1867         state.sel_from = 0;
1868         sel_text(59);
1869         s = cook(state.cur_note, 0, 59);
1870         cb_copy(s, -1);
1871         free(s);
1872         return;
1873       case XK_r: case XK_R:  /* copy raw */
1874         state.sel_from = 0;
1875         sel_text(59);
1876         cb_copy(ndata[state.cur_note].text, -1);
1877         return;
1878       case XK_i: case XK_I:  /* paste raw */
1879         state.raw_paste = 1;
1880         cb_paste(state.cur_note, state.insert);
1881         init_edit_mode(state.cur_note);
1882         set_cursor(ndata[state.cur_note].cursor, 1);
1883         return;
1884       case XK_y: case XK_Y: case XK_z: case XK_Z:  /* zap line */
1885         for (i = 10*(ndata[state.cur_note].cursor/10); i < 59; i++)
1886           ndata[state.cur_note].text[i] =
1887             i < 49 ? ndata[state.cur_note].text[i+10] : ' ';
1888         init_edit_mode(state.cur_note);
1889         set_cursor(10*(ndata[state.cur_note].cursor/10), 1);
1890         return;
1891       case XK_n: case XK_N:  /* EMACS-style down */
1892         ksym = XK_Down;
1893         break;
1894       case XK_p: case XK_P:  /* EMACS-style up */
1895         ksym = XK_Up;
1896         break;
1897       case XK_f: case XK_F:  /* EMACS-style right */
1898         ksym = XK_Right;
1899         break;
1900       case XK_b: case XK_B:  /* EMACS-style left */
1901         ksym = XK_Left;
1902         break;
1903       default:
1904         return;
1905     }
1906   } else {
1907     if (InputContext) {
1908       j = XmbLookupString(InputContext, &event->xkey, (char*) ch, sizeof(ch),
1909         &ksym, 0);
1910     } else {
1911       j = XLookupString(&event->xkey, (char*) ch, sizeof(ch), &ksym,
1912         &state.compose);
1913     }
1914     if (!j && (ksym & 0xff00) == 0x0600) {  /* cyrillic keysyms */
1915       ch[0] = (unsigned char) (ksym & 0x00ff);
1916       ch[1] = '\0';
1917       if (ch[0] >= 0xbf || ch[0] == 0xa3 || ch[0] == 0xb3)
1918         j = 1;  /* filter extended KOI8 characters */
1919     }
1920   }
1921   switch (ksym) {
1922     case XK_Return: case XK_KP_Enter:
1923       if (ndata[state.cur_note].cursor >= 50) break;
1924       if (state.insert) {
1925         shift_string(state.cur_note, 10*(ndata[state.cur_note].cursor/10+1), 58,
1926           ndata[state.cur_note].cursor%10, ' ');
1927         shift_string(state.cur_note, ndata[state.cur_note].cursor, 58,
1928           10-ndata[state.cur_note].cursor%10, ' ');
1929       }
1930       set_cursor(10*(ndata[state.cur_note].cursor/10+1), 1);
1931       break;
1932     case XK_BackSpace: case 0xfd1a:
1933       if (!ndata[state.cur_note].cursor) break;
1934       if (ndata[state.cur_note].cursor == 58 &&
1935         ndata[state.cur_note].text[58] != ' ')
1936       { /* special behavior when on very last character */
1937         ndata[state.cur_note].text[58] = ' ';
1938         set_cursor(ndata[state.cur_note].cursor, 1);
1939         break;
1940       }
1941       print_letter(state.cur_note, ndata[state.cur_note].cursor--, 1);
1942     case XK_Delete: case XK_KP_Delete:
1943       j = 1;  /* delete entire line if empty... */
1944       if (((ksym != XK_BackSpace && ksym != 0xfd1a) ||
1945         ndata[state.cur_note].cursor%10 == 9) &&
1946         string_empty(&ndata[state.cur_note].text[10*
1947           (ndata[state.cur_note].cursor/10)], 10))
1948       {
1949         ndata[state.cur_note].cursor = 10*(ndata[state.cur_note].cursor/10);
1950         j = 10;
1951       }
1952       if (ksym == XK_BackSpace || ksym == 0xfd1a) {
1953         if (ndata[state.cur_note].cursor%10 == 9 &&
1954           ndata[state.cur_note].text[ndata[state.cur_note].cursor] == ' ')
1955         {
1956           for (; ndata[state.cur_note].text[ndata[state.cur_note].cursor-1] ==
1957             ' ' && ndata[state.cur_note].cursor >
1958             10*(ndata[state.cur_note].cursor/10);)
1959           {
1960             ndata[state.cur_note].text[ndata[state.cur_note].cursor--] = ' ';
1961           }
1962         }
1963       }
1964 
1965       for (i = ndata[state.cur_note].cursor; i < (j == 1 &&
1966         ndata[state.cur_note].cursor/10 < 5 ?
1967         10*(ndata[state.cur_note].cursor/10+1)-j : 59-j); i++)
1968       {
1969         ndata[state.cur_note].text[i] = ndata[state.cur_note].text[i+j];
1970         ndata[state.cur_note].text[i+j] = ' ';
1971         print_letter(state.cur_note, i, 1);
1972       }
1973       if (ndata[state.cur_note].cursor%10 == 9 ||
1974         ndata[state.cur_note].cursor == 58)
1975       { /* exceptions to the loop */
1976         ndata[state.cur_note].text[ndata[state.cur_note].cursor] = ' ';
1977       }
1978       if (j > 1)  /* only when lines have been shifted */
1979         init_edit_mode(state.cur_note);
1980       else
1981         print_letter(state.cur_note, i, 1);
1982       set_cursor(ndata[state.cur_note].cursor, 1);
1983       break;
1984     case XK_Up: case XK_KP_Up:
1985       if ((event->xkey.state & 0x01) == 0x01) {  /* next note w/similar color */
1986         for (i = state.cur_note == notes_count-1 ? 0 : state.cur_note+1;
1987            i != state.cur_note;)
1988         {
1989            if (c_group[ndata[i].col] == c_group[ndata[state.cur_note].col])
1990              break;
1991            if (++i >= notes_count) i = 0;
1992         }
1993         if (i == state.cur_note) break;
1994         if (note_empty(state.cur_note)) remove_note(state.cur_note);
1995         notes_io(1);
1996         state.cur_note = i;
1997         animate_note(2);
1998       } else  /* move ndata[state.cur_note].cursor */
1999         if (ndata[state.cur_note].cursor > 9)
2000           set_cursor(ndata[state.cur_note].cursor-10, 1);
2001       break;
2002     case XK_Down: case XK_KP_Down:
2003       if ((event->xkey.state & 0x01) == 0x01) {  /* prev. note w/simil. color */
2004         for (i = state.cur_note ? state.cur_note-1 : notes_count-1;
2005           i != state.cur_note;)
2006         {
2007            if (c_group[ndata[i].col] == c_group[ndata[state.cur_note].col])
2008              break;
2009            if (--i < 0) i = notes_count-1;
2010         }
2011         if (i == state.cur_note) break;
2012         if (note_empty(state.cur_note)) remove_note(state.cur_note);
2013         notes_io(1);
2014         state.cur_note = i;
2015         animate_note(3);
2016       } else  /* move ndata[state.cur_note].cursor */
2017         if (ndata[state.cur_note].cursor < 49)
2018           set_cursor(ndata[state.cur_note].cursor+10, 1);
2019       break;
2020     case XK_Left: case XK_KP_Left:
2021       if ((event->xkey.state & 0x01) == 0x01) {  /* previous note */
2022         if (notes_count == 1) break;
2023         if (note_empty(state.cur_note)) remove_note(state.cur_note);
2024         notes_io(1);
2025         if (--state.cur_note < 0) state.cur_note = notes_count-1;
2026         animate_note(1);
2027       } else  /* move cursor */
2028         if (ndata[state.cur_note].cursor)
2029           set_cursor(ndata[state.cur_note].cursor-1, 1);
2030       break;
2031     case XK_Right: case XK_KP_Right:
2032       if ((event->xkey.state & 0x01) == 0x01) {  /* next note */
2033         if (notes_count == 1) break;
2034         if (note_empty(state.cur_note)) {
2035           remove_note(state.cur_note);
2036           state.cur_note = notes_count-1;
2037         }
2038         notes_io(1);
2039         if (++state.cur_note == notes_count) state.cur_note = 0;
2040         animate_note(0);
2041       } else  /* move cursor */
2042         if (ndata[state.cur_note].cursor < 58)
2043           set_cursor(ndata[state.cur_note].cursor+1, 1);
2044       break;
2045     case XK_Tab: case XK_ISO_Left_Tab: /* change color */
2046       i = ndata[state.cur_note].col;
2047       if ((ksym == XK_Tab && (event->xkey.state & 0x01) == 0x01) ||
2048         ksym == XK_ISO_Left_Tab)
2049       {  /* previous color */
2050         if (--ndata[state.cur_note].col < 0)
2051           ndata[state.cur_note].col = C_NUM-1;
2052       } else  if (++ndata[state.cur_note].col == C_NUM)
2053         ndata[state.cur_note].col = 0;
2054       init_edit_mode(state.cur_note);
2055       set_cursor(ndata[state.cur_note].cursor, 0);
2056       redraw_window();
2057       break;
2058     case XK_Home: case XK_KP_Home:
2059       set_cursor(10*(ndata[state.cur_note].cursor/10), 1);
2060       break;
2061     case XK_End: case XK_KP_End:
2062       i = j = ndata[state.cur_note].cursor < 50 ?
2063         10*(ndata[state.cur_note].cursor/10)+9 : 58;
2064       if (ndata[state.cur_note].text[j] == ' ')
2065         for (; i > 10*(j/10) && ndata[state.cur_note].text[i-1] == ' '; i--);
2066       set_cursor(i, 1);
2067       break;
2068     case XK_Prior: case XK_KP_Prior:  /* page up */
2069       set_cursor(0, 1);
2070       break;
2071     case XK_Next: case XK_KP_Next:  /* page down */
2072       set_cursor(58, 1);
2073       break;
2074     case XK_Insert: case XK_KP_Insert:
2075       state.insert = !state.insert;
2076       set_cursor(ndata[state.cur_note].cursor, 1);
2077       break;
2078     case XK_Escape:
2079       quit_edit_mode(0, 1);
2080       if (!opts.click_to_focus) set_kbfocus(0);
2081       break;
2082     default:
2083       if (j) {
2084         if (state.insert)
2085           shift_string(state.cur_note, ndata[state.cur_note].cursor,
2086             ndata[state.cur_note].cursor < 50 ?
2087             10*(ndata[state.cur_note].cursor/10)+9 : 58, 1, ch[0]);
2088         else {
2089           ndata[state.cur_note].text[ndata[state.cur_note].cursor] = ch[0];
2090           print_letter(state.cur_note, ndata[state.cur_note].cursor, 1);
2091         }
2092         set_cursor(ndata[state.cur_note].cursor < 58 ?
2093           ndata[state.cur_note].cursor+1 : ndata[state.cur_note].cursor, 1);
2094       }
2095   }
2096 }
2097                                    /********/
2098 /*********************************** MAIN ************************************/
2099                                  /********/
2100 int
main(int argc,char ** argv)2101 main(int argc, char **argv)
2102 {
2103 #ifdef FUNSTUFF
2104   time_t tt = 0, t;
2105   struct tm ltt, lt;
2106 #endif
2107   XEvent event;
2108   XGCValues gcv;
2109   int i, j;
2110 
2111   srand(time(0));
2112   umask(077);  /* users' notes needn't be world-readable */
2113 
2114   parse_argv(argc, argv);  /* evaluate command line parameters */
2115   init();                  /* initialize X window */
2116   XSetCommand(display, mainwin, argv, argc);
2117   XMapWindow(display, mainwin);
2118   init_xlocale();          /* initialize input context */
2119 
2120   /* initialize internal images, palette, cursors */
2121   bbar = get_xpm((char**) bbar_xpm);
2122   abar = get_xpm((char**) abar_xpm);
2123   digits = get_xpm((char**) digits_xpm);
2124   img = XGetImage(display, app, 0, 0, 86, 64, ~0, ZPixmap);
2125   XGetSubImage(display, abar, 47, 4, 8, 8, ~0, ZPixmap, img, 70, 55);
2126   XGetSubImage(display, abar, 47, 16, 8, 8, ~0, ZPixmap, img, 78, 55);
2127   for (i = 0; i < C_NUM; i++) {
2128     palette[i].bg = XGetPixel(img, 80, i);
2129     palette[i].fg = XGetPixel(img, 81, i);
2130     palette[i].cr = XGetPixel(img, 82, i);
2131   }
2132   palette[C_NUM].fg = XGetPixel(img, 80, 63);  /* C_INNER */
2133   palette[C_NUM].bg = XGetPixel(img, 81, 63);  /* C_OUTER */
2134   palette[C_NUM].cr = XGetPixel(img, 82, 63);  /* C_EXTRA */
2135   gcv.foreground = C_OUTER;  /* dummy value */
2136   gcv.graphics_exposures = 0;
2137   fillGC = XCreateGC(display, app, GCForeground | GCGraphicsExposures, &gcv);
2138   cursors[0] = XCreateFontCursor(display, XC_fleur);  /* when moving a note */
2139   cursors[1] = XCreateFontCursor(display, XC_dotbox);  /* drag-create */
2140   cursors[2] = XCreateFontCursor(display, XC_pencil);  /* for sketch mode */
2141 
2142   load_font();
2143   load_theme(opts.theme);
2144   notes_io(1);  /* saves PID */
2145   render_pinboard(-1);
2146   redraw_window();
2147   set_mode(M_NOOP);
2148 
2149   for(;;) {  /*** MAIN EVENT LOOP ***/
2150     while (XPending(display)) {
2151       XNextEvent(display, &event);
2152 
2153       s_block();  /* BEGIN OF SYNC-PROHIBITING SECTION */
2154 
2155       switch (event.type) {
2156         case Expose:
2157           redraw_window();
2158           break;
2159         case ButtonPress:
2160           handle_ButtonPress(&event);
2161           time(&state.idle);
2162           break;
2163         case ButtonRelease:
2164           if (event.xbutton.button == state.button)
2165             handle_ButtonRelease(&event);
2166           time(&state.idle);
2167           break;
2168         case MotionNotify:
2169           handle_MotionNotify(&event);
2170           break;
2171         case EnterNotify:
2172           if (state.mode == M_EDIT && !opts.click_to_focus) set_kbfocus(1);
2173           state.clicks_count = 0;
2174           break;
2175         case LeaveNotify:
2176           if (state.mode == M_EDIT && !opts.click_to_focus) set_kbfocus(0);
2177           break;
2178         case KeyPress:
2179           handle_KeyPress(&event);
2180           break;
2181         case SelectionClear:
2182           clear_selection();
2183           cb_clear();
2184           break;
2185         case SelectionNotify:
2186           if (state.mode == M_EDIT) {
2187             cb_paste_external(event.xselection.requestor,
2188               event.xselection.property, 1, state.cur_note, state.insert,
2189               state.raw_paste);
2190             init_edit_mode(state.cur_note);
2191             set_cursor(ndata[state.cur_note].cursor, 1);
2192           }
2193           break;
2194         case SelectionRequest:
2195           handle_SelectionRequest(&(event.xselectionrequest));
2196       }
2197     }
2198     if (opts.timeout && (state.mode == M_EDIT || state.mode == M_BBAR ||
2199       state.mode == M_DRAW || state.mode == M_ERAS) &&
2200       time(0)-state.idle > opts.timeout)
2201     {
2202       quit_edit_mode(0, 1);  /* no set_kbfocus(0) here */
2203     }
2204 #ifdef FUNSTUFF
2205     if (state.mode == M_NOOP) {
2206       time(&t);
2207       if (t-tt > 10*60 && localtime_r(&tt, &ltt) && localtime_r(&t, &lt)) {
2208         if (ltt.tm_mday != lt.tm_mday || ltt.tm_mon != lt.tm_mon ||
2209           ltt.tm_year != lt.tm_year)
2210         {
2211           i = notes_count;
2212           j = state.state_bits;
2213           check_occasion(lt.tm_mday, 1+lt.tm_mon, 1900+lt.tm_year);
2214           if (i != notes_count || j != state.state_bits) {  /* anything done? */
2215             notes_io(1);
2216             render_pinboard(-1);
2217             redraw_window();
2218           }
2219         }
2220         tt = t;
2221       }
2222     }
2223 #endif
2224     if (state.mode == M_ALRM)
2225       animate_alarm();
2226     else if (state.mode == M_NOOP && state.alarm.note >= 0 &&
2227       state.alarm.time <= time(0))
2228     {  /* alarm due */
2229       state.cur_note = state.alarm.note;
2230       state.alarm.run = 1;
2231       if (ndata[state.cur_note].a_flags & ALARM_DATE)
2232         ndata[state.cur_note].a_flags &= ~ALARM_ON;
2233       animate_note(4);
2234       set_mode(M_ALRM);
2235       if (strlen(opts.alarm_cmd)) {
2236         char buf[STRING_BUF_SIZE+2];
2237         strcpy(buf, opts.alarm_cmd);
2238         strcat(buf, " &");
2239         system(buf);
2240       }
2241       prepare_alarm_anim();
2242       state.alarm.phase = 0;
2243       animate_alarm();
2244     }
2245 
2246     s_unblock();  /* END OF SYNC-PROHIBITING SECTION */
2247 
2248     /* sleep for a while... */
2249     switch (state.mode) {
2250       case M_NOOP: usleep(100000L); break;
2251       case M_ALRM: usleep(500000L); break;  /* animation timing, 0.5s */
2252       default:     usleep( 10000L);
2253     }
2254   }
2255 }
2256 
2257 /*
2258                             CONCEPTUAL NOTES
2259 
2260                                                       bbar =
2261                                   show panel        +---------+
2262  mainwin[, iconwin]        ,------------------------|::panel::| 30
2263       (each) =             |       app =            |:::::::::|
2264     +----------+           |  +----------+--+       +---------+
2265     |          |           `->|          |::|           58     \
2266     | visible  |    display   |   draw   |::| 48              dto. for abar
2267  64 |   area   | <=========== |  buffer  |::|
2268     |          |     64x64    |          +-++
2269     |          |          ,-->|         <->|| 16
2270     +----------+          |   +----------+-++
2271          64               |        64    16\3
2272                           |                 \ used when
2273                           |                 moving a note     server-side
2274   ........................|...............................................
2275                           |
2276               restore     |                                   client-side
2277             ,----------->-+
2278             |  board      |    draw
2279      img =  |   16        +-<--------[bitfield]
2280     +----------+-++  pin  |   sketch
2281     |::::::::::| |------>-+
2282     |:copy:of::| || notes |
2283  64 |:pristine:| ||       |
2284     |:pinboard:+++|  copy |
2285     |:::::::10{||---<--->-'
2286     +----------++-+ character/write it back
2287          64    6\16
2288                 /\                                            :: = const
2289                /  \ used to overlay a
2290               / character with the cursor
2291              /
2292     buffer of (8+8)x8 @ 70,55 for alarm panel switches
2293 */
2294 
2295