xref: /freebsd/contrib/dialog/calendar.c (revision cc16dea626cf2fc80cde667ac4798065108e596c)
1 /*
2  * $Id: calendar.c,v 1.67 2013/03/17 15:03:41 tom Exp $
3  *
4  *  calendar.c -- implements the calendar box
5  *
6  *  Copyright 2001-2012,2013	Thomas E. Dickey
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU Lesser General Public License, version 2.1
10  *  as published by the Free Software Foundation.
11  *
12  *  This program is distributed in the hope that it will be useful, but
13  *  WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  *  Lesser General Public License for more details.
16  *
17  *  You should have received a copy of the GNU Lesser General Public
18  *  License along with this program; if not, write to
19  *	Free Software Foundation, Inc.
20  *	51 Franklin St., Fifth Floor
21  *	Boston, MA 02110, USA.
22  */
23 
24 #include <dialog.h>
25 #include <dlg_keys.h>
26 
27 #include <time.h>
28 
29 #define ONE_DAY  (60 * 60 * 24)
30 
31 #define MON_WIDE 4		/* width of a month-name */
32 #define DAY_HIGH 6		/* maximum lines in day-grid */
33 #define DAY_WIDE (8 * MON_WIDE)	/* width of the day-grid */
34 #define HDR_HIGH 1		/* height of cells with month/year */
35 #define BTN_HIGH 1		/* height of button-row excluding margin */
36 
37 /* two more lines: titles for day-of-week and month/year boxes */
38 #define MIN_HIGH (DAY_HIGH + 2 + HDR_HIGH + BTN_HIGH + (7 * MARGIN))
39 #define MIN_WIDE (DAY_WIDE + (4 * MARGIN))
40 
41 typedef enum {
42     sMONTH = -3
43     ,sYEAR = -2
44     ,sDAY = -1
45 } STATES;
46 
47 struct _box;
48 
49 typedef int (*BOX_DRAW) (struct _box *, struct tm *);
50 
51 typedef struct _box {
52     WINDOW *parent;
53     WINDOW *window;
54     int x;
55     int y;
56     int width;
57     int height;
58     BOX_DRAW box_draw;
59 } BOX;
60 
61 static const char *
62 nameOfDayOfWeek(int n)
63 {
64     static const char *table[7]
65 #ifndef ENABLE_NLS
66     =
67     {
68 	"Sunday",
69 	"Monday",
70 	"Tuesday",
71 	"Wednesday",
72 	"Thursday",
73 	"Friday",
74 	"Saturday"
75     }
76 #endif
77      ;
78     const char *result = 0;
79 
80     if (n >= 0 && n < 7) {
81 #ifdef ENABLE_NLS
82 	if (table[n] == 0) {
83 	    nl_item items[7] =
84 	    {
85 		ABDAY_1, ABDAY_2, ABDAY_3, ABDAY_4, ABDAY_5, ABDAY_6, ABDAY_7
86 	    };
87 	    table[n] = nl_langinfo(items[n]);
88 	}
89 #endif
90 	result = table[n];
91     }
92     if (result == 0) {
93 	result = "?";
94     }
95     return result;
96 }
97 
98 static const char *
99 nameOfMonth(int n)
100 {
101     static const char *table[12]
102 #ifndef ENABLE_NLS
103     =
104     {
105 	"January",
106 	"February",
107 	"March",
108 	"April",
109 	"May",
110 	"June",
111 	"July",
112 	"August",
113 	"September",
114 	"October",
115 	"November",
116 	"December"
117     }
118 #endif
119      ;
120     const char *result = 0;
121 
122     if (n >= 0 && n < 12) {
123 #ifdef ENABLE_NLS
124 	if (table[n] == 0) {
125 	    nl_item items[12] =
126 	    {
127 		MON_1, MON_2, MON_3, MON_4, MON_5, MON_6,
128 		MON_7, MON_8, MON_9, MON_10, MON_11, MON_12
129 	    };
130 	    table[n] = nl_langinfo(items[n]);
131 	}
132 #endif
133 	result = table[n];
134     }
135     if (result == 0) {
136 	result = "?";
137     }
138     return result;
139 }
140 
141 static int
142 days_in_month(struct tm *current, int offset /* -1, 0, 1 */ )
143 {
144     static const int nominal[] =
145     {
146 	31, 28, 31, 30, 31, 30,
147 	31, 31, 30, 31, 30, 31
148     };
149     int year = current->tm_year;
150     int month = current->tm_mon + offset;
151     int result;
152 
153     while (month < 0) {
154 	month += 12;
155 	year -= 1;
156     }
157     while (month >= 12) {
158 	month -= 12;
159 	year += 1;
160     }
161     result = nominal[month];
162     if (month == 1)
163 	result += ((year % 4) == 0);
164     return result;
165 }
166 
167 static int
168 days_in_year(struct tm *current, int offset /* -1, 0, 1 */ )
169 {
170     int year = current->tm_year + 1900 + offset;
171 
172     return ((year % 4) == 0) ? 366 : 365;
173 }
174 
175 static int
176 day_cell_number(struct tm *current)
177 {
178     int cell;
179     cell = current->tm_mday - ((6 + current->tm_mday - current->tm_wday) % 7);
180     if ((current->tm_mday - 1) % 7 != current->tm_wday)
181 	cell += 6;
182     else
183 	cell--;
184     return cell;
185 }
186 
187 static int
188 next_or_previous(int key, int two_d)
189 {
190     int result = 0;
191 
192     switch (key) {
193     case DLGK_GRID_UP:
194 	result = two_d ? -7 : -1;
195 	break;
196     case DLGK_GRID_LEFT:
197 	result = -1;
198 	break;
199     case DLGK_GRID_DOWN:
200 	result = two_d ? 7 : 1;
201 	break;
202     case DLGK_GRID_RIGHT:
203 	result = 1;
204 	break;
205     default:
206 	beep();
207 	break;
208     }
209     return result;
210 }
211 
212 /*
213  * Draw the day-of-month selection box
214  */
215 static int
216 draw_day(BOX * data, struct tm *current)
217 {
218     int cell_wide = MON_WIDE;
219     int y, x, this_x = 0;
220     int save_y = 0, save_x = 0;
221     int day = current->tm_mday;
222     int mday;
223     int week;
224     int last = days_in_month(current, 0);
225     int prev = days_in_month(current, -1);
226 
227     werase(data->window);
228     dlg_draw_box2(data->parent,
229 		  data->y - MARGIN, data->x - MARGIN,
230 		  data->height + (2 * MARGIN), data->width + (2 * MARGIN),
231 		  menubox_attr,
232 		  menubox_border_attr,
233 		  menubox_border2_attr);
234 
235     (void) wattrset(data->window, menubox_attr);	/* daynames headline */
236     for (x = 0; x < 7; x++) {
237 	mvwprintw(data->window,
238 		  0, (x + 1) * cell_wide, "%*.*s ",
239 		  cell_wide - 1,
240 		  cell_wide - 1,
241 		  nameOfDayOfWeek(x));
242     }
243 
244     mday = ((6 + current->tm_mday - current->tm_wday) % 7) - 7;
245     if (mday <= -7)
246 	mday += 7;
247     /* mday is now in the range -6 to 0. */
248     week = (current->tm_yday + 6 + mday - current->tm_mday) / 7;
249 
250     for (y = 1; mday < last; y++) {
251 	(void) wattrset(data->window, menubox_attr);	/* weeknumbers headline */
252 	mvwprintw(data->window,
253 		  y, 0,
254 		  "%*d ",
255 		  cell_wide - 1,
256 		  ++week);
257 	for (x = 0; x < 7; x++) {
258 	    this_x = 1 + (x + 1) * cell_wide;
259 	    ++mday;
260 	    if (wmove(data->window, y, this_x) == ERR)
261 		continue;
262 	    (void) wattrset(data->window, item_attr);	/* not selected days */
263 	    if (mday == day) {
264 		(void) wattrset(data->window, item_selected_attr);	/* selected day */
265 		save_y = y;
266 		save_x = this_x;
267 	    }
268 	    if (mday > 0) {
269 		if (mday <= last) {
270 		    wprintw(data->window, "%*d", cell_wide - 2, mday);
271 		} else if (mday == day) {
272 		    wprintw(data->window, "%*d", cell_wide - 2, mday - last);
273 		}
274 	    } else if (mday == day) {
275 		wprintw(data->window, "%*d", cell_wide - 2, mday + prev);
276 	    }
277 	}
278 	wmove(data->window, save_y, save_x);
279     }
280     /* just draw arrows - scrollbar is unsuitable here */
281     dlg_draw_arrows(data->parent, TRUE, TRUE,
282 		    data->x + ARROWS_COL,
283 		    data->y - 1,
284 		    data->y + data->height);
285 
286     return 0;
287 }
288 
289 /*
290  * Draw the month-of-year selection box
291  */
292 static int
293 draw_month(BOX * data, struct tm *current)
294 {
295     int month;
296 
297     month = current->tm_mon + 1;
298 
299     (void) wattrset(data->parent, dialog_attr);		/* Headline "Month" */
300     (void) mvwprintw(data->parent, data->y - 2, data->x - 1, _("Month"));
301     dlg_draw_box2(data->parent,
302 		  data->y - 1, data->x - 1,
303 		  data->height + 2, data->width + 2,
304 		  menubox_attr,
305 		  menubox_border_attr,
306 		  menubox_border2_attr);
307     (void) wattrset(data->window, item_attr);	/* color the month selection */
308     mvwprintw(data->window, 0, 0, "%s", nameOfMonth(month - 1));
309     wmove(data->window, 0, 0);
310     return 0;
311 }
312 
313 /*
314  * Draw the year selection box
315  */
316 static int
317 draw_year(BOX * data, struct tm *current)
318 {
319     int year = current->tm_year + 1900;
320 
321     (void) wattrset(data->parent, dialog_attr);		/* Headline "Year" */
322     (void) mvwprintw(data->parent, data->y - 2, data->x - 1, _("Year"));
323     dlg_draw_box2(data->parent,
324 		  data->y - 1, data->x - 1,
325 		  data->height + 2, data->width + 2,
326 		  menubox_attr,
327 		  menubox_border_attr,
328 		  menubox_border2_attr);
329     (void) wattrset(data->window, item_attr);	/* color the year selection */
330     mvwprintw(data->window, 0, 0, "%4d", year);
331     wmove(data->window, 0, 0);
332     return 0;
333 }
334 
335 static int
336 init_object(BOX * data,
337 	    WINDOW *parent,
338 	    int x, int y,
339 	    int width, int height,
340 	    BOX_DRAW box_draw,
341 	    int code)
342 {
343     data->parent = parent;
344     data->x = x;
345     data->y = y;
346     data->width = width;
347     data->height = height;
348     data->box_draw = box_draw;
349 
350     data->window = derwin(data->parent,
351 			  data->height, data->width,
352 			  data->y, data->x);
353     if (data->window == 0)
354 	return -1;
355     (void) keypad(data->window, TRUE);
356 
357     dlg_mouse_setbase(getbegx(parent), getbegy(parent));
358     if (code == 'D') {
359 	dlg_mouse_mkbigregion(y + 1, x + MON_WIDE, height - 1, width - MON_WIDE,
360 			      KEY_MAX, 1, MON_WIDE, 3);
361     } else {
362 	dlg_mouse_mkregion(y, x, height, width, code);
363     }
364 
365     return 0;
366 }
367 
368 static int
369 CleanupResult(int code, WINDOW *dialog, char *prompt, DIALOG_VARS * save_vars)
370 {
371     if (dialog != 0)
372 	dlg_del_window(dialog);
373     dlg_mouse_free_regions();
374     if (prompt != 0)
375 	free(prompt);
376     dlg_restore_vars(save_vars);
377 
378     return code;
379 }
380 
381 #define DrawObject(data) (data)->box_draw(data, &current)
382 
383 /*
384  * Display a dialog box for entering a date
385  */
386 int
387 dialog_calendar(const char *title,
388 		const char *subtitle,
389 		int height,
390 		int width,
391 		int day,
392 		int month,
393 		int year)
394 {
395     /* *INDENT-OFF* */
396     static DLG_KEYS_BINDING binding[] = {
397 	HELPKEY_BINDINGS,
398 	ENTERKEY_BINDINGS,
399 	DLG_KEYS_DATA( DLGK_ENTER,	' ' ),
400 	DLG_KEYS_DATA( DLGK_FIELD_NEXT, TAB ),
401 	DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_BTAB ),
402 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	'j' ),
403 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	DLGK_MOUSE(KEY_NPAGE) ),
404 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	KEY_DOWN ),
405 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	KEY_NPAGE ),
406 	DLG_KEYS_DATA( DLGK_GRID_LEFT,	'-' ),
407 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  'h' ),
408 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  CHR_BACKSPACE ),
409 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  CHR_PREVIOUS ),
410 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  KEY_LEFT ),
411 	DLG_KEYS_DATA( DLGK_GRID_RIGHT,	'+' ),
412 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, 'l' ),
413 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, CHR_NEXT ),
414 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_NEXT ),
415 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_RIGHT ),
416 	DLG_KEYS_DATA( DLGK_GRID_UP,	'k' ),
417 	DLG_KEYS_DATA( DLGK_GRID_UP,	KEY_PPAGE ),
418 	DLG_KEYS_DATA( DLGK_GRID_UP,	KEY_PREVIOUS ),
419 	DLG_KEYS_DATA( DLGK_GRID_UP,	KEY_UP ),
420 	DLG_KEYS_DATA( DLGK_GRID_UP,  	DLGK_MOUSE(KEY_PPAGE) ),
421 	END_KEYS_BINDING
422     };
423     /* *INDENT-ON* */
424 
425 #ifdef KEY_RESIZE
426     int old_height = height;
427     int old_width = width;
428 #endif
429     BOX dy_box, mn_box, yr_box;
430     int fkey;
431     int key = 0;
432     int key2;
433     int step;
434     int button;
435     int result = DLG_EXIT_UNKNOWN;
436     WINDOW *dialog;
437     time_t now_time = time((time_t *) 0);
438     struct tm current;
439     int state = dlg_default_button();
440     const char **buttons = dlg_ok_labels();
441     char *prompt = dlg_strclone(subtitle);
442     int mincols = MIN_WIDE;
443     char buffer[MAX_LEN];
444     DIALOG_VARS save_vars;
445 
446     dlg_save_vars(&save_vars);
447     dialog_vars.separate_output = TRUE;
448 
449     dlg_does_output();
450 
451     now_time = time((time_t *) 0);
452     current = *localtime(&now_time);
453     if (day < 0)
454 	day = current.tm_mday;
455     if (month < 0)
456 	month = current.tm_mon + 1;
457     if (year < 0)
458 	year = current.tm_year + 1900;
459 
460     /* compute a struct tm that matches the day/month/year parameters */
461     if (((year -= 1900) > 0) && (year < 200)) {
462 	/* ugly, but I'd like to run this on older machines w/o mktime -TD */
463 	for (;;) {
464 	    if (year > current.tm_year) {
465 		now_time += ONE_DAY * days_in_year(&current, 0);
466 	    } else if (year < current.tm_year) {
467 		now_time -= ONE_DAY * days_in_year(&current, -1);
468 	    } else if (month > current.tm_mon + 1) {
469 		now_time += ONE_DAY * days_in_month(&current, 0);
470 	    } else if (month < current.tm_mon + 1) {
471 		now_time -= ONE_DAY * days_in_month(&current, -1);
472 	    } else if (day > current.tm_mday) {
473 		now_time += ONE_DAY;
474 	    } else if (day < current.tm_mday) {
475 		now_time -= ONE_DAY;
476 	    } else {
477 		break;
478 	    }
479 	    current = *localtime(&now_time);
480 	}
481     }
482     dlg_button_layout(buttons, &mincols);
483 
484 #ifdef KEY_RESIZE
485   retry:
486 #endif
487 
488     dlg_auto_size(title, prompt, &height, &width, 0, mincols);
489     height += MIN_HIGH - 1;
490     dlg_print_size(height, width);
491     dlg_ctl_size(height, width);
492 
493     dialog = dlg_new_window(height, width,
494 			    dlg_box_y_ordinate(height),
495 			    dlg_box_x_ordinate(width));
496     dlg_register_window(dialog, "calendar", binding);
497     dlg_register_buttons(dialog, "calendar", buttons);
498 
499     /* mainbox */
500     dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr);
501     dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr);
502     dlg_draw_title(dialog, title);
503 
504     (void) wattrset(dialog, dialog_attr);	/* text mainbox */
505     dlg_print_autowrap(dialog, prompt, height, width);
506 
507     /* compute positions of day, month and year boxes */
508     memset(&dy_box, 0, sizeof(dy_box));
509     memset(&mn_box, 0, sizeof(mn_box));
510     memset(&yr_box, 0, sizeof(yr_box));
511 
512     if (init_object(&dy_box,
513 		    dialog,
514 		    (width - DAY_WIDE) / 2,
515 		    1 + (height - (DAY_HIGH + BTN_HIGH + (5 * MARGIN))),
516 		    DAY_WIDE,
517 		    DAY_HIGH + 1,
518 		    draw_day,
519 		    'D') < 0
520 	|| DrawObject(&dy_box) < 0) {
521 	return CleanupResult(DLG_EXIT_ERROR, dialog, prompt, &save_vars);
522     }
523 
524     if (init_object(&mn_box,
525 		    dialog,
526 		    dy_box.x,
527 		    dy_box.y - (HDR_HIGH + 2 * MARGIN),
528 		    (DAY_WIDE / 2) - MARGIN,
529 		    HDR_HIGH,
530 		    draw_month,
531 		    'M') < 0
532 	|| DrawObject(&mn_box) < 0) {
533 	return CleanupResult(DLG_EXIT_ERROR, dialog, prompt, &save_vars);
534     }
535 
536     if (init_object(&yr_box,
537 		    dialog,
538 		    dy_box.x + mn_box.width + 2,
539 		    mn_box.y,
540 		    mn_box.width,
541 		    mn_box.height,
542 		    draw_year,
543 		    'Y') < 0
544 	|| DrawObject(&yr_box) < 0) {
545 	return CleanupResult(DLG_EXIT_ERROR, dialog, prompt, &save_vars);
546     }
547 
548     dlg_trace_win(dialog);
549     while (result == DLG_EXIT_UNKNOWN) {
550 	BOX *obj = (state == sDAY ? &dy_box
551 		    : (state == sMONTH ? &mn_box :
552 		       (state == sYEAR ? &yr_box : 0)));
553 
554 	button = (state < 0) ? 0 : state;
555 	dlg_draw_buttons(dialog, height - 2, 0, buttons, button, FALSE, width);
556 	if (obj != 0)
557 	    dlg_set_focus(dialog, obj->window);
558 
559 	key = dlg_mouse_wgetch(dialog, &fkey);
560 	if (dlg_result_key(key, fkey, &result))
561 	    break;
562 
563 	if (fkey && (key >= DLGK_MOUSE(KEY_MIN) && key <= DLGK_MOUSE(KEY_MAX))) {
564 	    key = dlg_lookup_key(dialog, key - M_EVENT, &fkey);
565 	}
566 
567 	if ((key2 = dlg_char_to_button(key, buttons)) >= 0) {
568 	    result = key2;
569 	} else if (fkey) {
570 	    /* handle function-keys */
571 	    switch (key) {
572 	    case DLGK_MOUSE('D'):
573 		state = sDAY;
574 		break;
575 	    case DLGK_MOUSE('M'):
576 		state = sMONTH;
577 		break;
578 	    case DLGK_MOUSE('Y'):
579 		state = sYEAR;
580 		break;
581 	    case DLGK_ENTER:
582 		result = dlg_enter_buttoncode(button);
583 		break;
584 	    case DLGK_FIELD_PREV:
585 		state = dlg_prev_ok_buttonindex(state, sMONTH);
586 		break;
587 	    case DLGK_FIELD_NEXT:
588 		state = dlg_next_ok_buttonindex(state, sMONTH);
589 		break;
590 #ifdef KEY_RESIZE
591 	    case KEY_RESIZE:
592 		/* reset data */
593 		height = old_height;
594 		width = old_width;
595 		/* repaint */
596 		dlg_clear();
597 		dlg_del_window(dialog);
598 		refresh();
599 		dlg_mouse_free_regions();
600 		goto retry;
601 #endif
602 	    default:
603 		step = 0;
604 		key2 = -1;
605 		if (is_DLGK_MOUSE(key)) {
606 		    if ((key2 = dlg_ok_buttoncode(key - M_EVENT)) >= 0) {
607 			result = key2;
608 			break;
609 		    } else if (key >= DLGK_MOUSE(KEY_MAX)) {
610 			state = sDAY;
611 			obj = &dy_box;
612 			key2 = 1;
613 			step = (key
614 				- DLGK_MOUSE(KEY_MAX)
615 				- day_cell_number(&current));
616 		    }
617 		}
618 		if (obj != 0) {
619 		    if (key2 < 0)
620 			step = next_or_previous(key, (obj == &dy_box));
621 		    if (step != 0) {
622 			struct tm old = current;
623 
624 			/* see comment regarding mktime -TD */
625 			if (obj == &dy_box) {
626 			    now_time += ONE_DAY * step;
627 			} else if (obj == &mn_box) {
628 			    if (step > 0)
629 				now_time += ONE_DAY *
630 				    days_in_month(&current, 0);
631 			    else
632 				now_time -= ONE_DAY *
633 				    days_in_month(&current, -1);
634 			} else if (obj == &yr_box) {
635 			    if (step > 0)
636 				now_time += (ONE_DAY
637 					     * days_in_year(&current, 0));
638 			    else
639 				now_time -= (ONE_DAY
640 					     * days_in_year(&current, -1));
641 			}
642 
643 			current = *localtime(&now_time);
644 
645 			if (obj != &dy_box
646 			    && (current.tm_mday != old.tm_mday
647 				|| current.tm_mon != old.tm_mon
648 				|| current.tm_year != old.tm_year))
649 			    DrawObject(&dy_box);
650 			if (obj != &mn_box && current.tm_mon != old.tm_mon)
651 			    DrawObject(&mn_box);
652 			if (obj != &yr_box && current.tm_year != old.tm_year)
653 			    DrawObject(&yr_box);
654 			(void) DrawObject(obj);
655 		    }
656 		} else if (state >= 0) {
657 		    if (next_or_previous(key, FALSE) < 0)
658 			state = dlg_prev_ok_buttonindex(state, sMONTH);
659 		    else if (next_or_previous(key, FALSE) > 0)
660 			state = dlg_next_ok_buttonindex(state, sMONTH);
661 		}
662 		break;
663 	    }
664 	}
665     }
666 
667 #define DefaultFormat(dst, src) \
668 	sprintf(dst, "%02d/%02d/%0d", \
669 		src.tm_mday, src.tm_mon + 1, src.tm_year + 1900)
670 #ifdef HAVE_STRFTIME
671     if (dialog_vars.date_format != 0) {
672 	size_t used = strftime(buffer,
673 			       sizeof(buffer) - 1,
674 			       dialog_vars.date_format,
675 			       &current);
676 	if (used == 0 || *buffer == '\0')
677 	    DefaultFormat(buffer, current);
678     } else
679 #endif
680 	DefaultFormat(buffer, current);
681 
682     dlg_add_result(buffer);
683     dlg_add_separator();
684     dlg_add_last_key(-1);
685 
686     return CleanupResult(result, dialog, prompt, &save_vars);
687 }
688