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, <t) && localtime_r(&t, <)) {
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