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
41typedef enum {
42    sMONTH = -3
43    ,sYEAR = -2
44    ,sDAY = -1
45} STATES;
46
47struct _box;
48
49typedef int (*BOX_DRAW) (struct _box *, struct tm *);
50
51typedef 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
61static const char *
62nameOfDayOfWeek(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
98static const char *
99nameOfMonth(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
141static int
142days_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
167static int
168days_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
175static int
176day_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
187static int
188next_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 */
215static int
216draw_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 */
292static int
293draw_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 */
316static int
317draw_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
335static int
336init_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
368static int
369CleanupResult(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 */
386int
387dialog_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