xref: /dragonfly/contrib/dialog/calendar.c (revision ec1c3f3a)
1 /*
2  * $Id: calendar.c,v 1.110 2022/04/03 22:38:16 tom Exp $
3  *
4  *  calendar.c -- implements the calendar box
5  *
6  *  Copyright 2001-2021,2022	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 <dlg_internals.h>
25 #include <dlg_keys.h>
26 
27 #define ONE_DAY  (60 * 60 * 24)
28 
29 #define MON_WIDE 4		/* width of a month-name */
30 #define DAY_HIGH 6		/* maximum lines in day-grid */
31 #define DAY_WIDE (8 * MON_WIDE)	/* width of the day-grid */
32 #define HDR_HIGH 1		/* height of cells with month/year */
33 #define BTN_HIGH 1		/* height of button-row excluding margin */
34 
35 /* two more lines: titles for day-of-week and month/year boxes */
36 #define MIN_HIGH (DAY_HIGH + 2 + HDR_HIGH + BTN_HIGH + (MAX_DAYS * MARGIN))
37 #define MIN_WIDE (DAY_WIDE + (4 * MARGIN))
38 
39 typedef enum {
40     sMONTH = -3
41     ,sYEAR = -2
42     ,sDAY = -1
43 } STATES;
44 
45 struct _box;
46 
47 typedef int (*BOX_DRAW) (struct _box *, struct tm *);
48 
49 typedef struct _box {
50     WINDOW *parent;
51     WINDOW *window;
52     int x;
53     int y;
54     int width;
55     int height;
56     BOX_DRAW box_draw;
57     int week_start;
58 } BOX;
59 
60 #define MAX_DAYS 7
61 #define MAX_MONTHS 12
62 
63 static char *cached_days[MAX_DAYS];
64 static char *cached_months[MAX_MONTHS];
65 
66 static const char *
67 nameOfDayOfWeek(int n)
68 {
69     static bool shown[MAX_DAYS];
70 
71     while (n < 0) {
72 	n += MAX_DAYS;
73     }
74     n %= MAX_DAYS;
75 #ifdef ENABLE_NLS
76     if (cached_days[n] == 0) {
77 	const nl_item items[MAX_DAYS] =
78 	{
79 	    ABDAY_1, ABDAY_2, ABDAY_3, ABDAY_4, ABDAY_5, ABDAY_6, ABDAY_7
80 	};
81 	cached_days[n] = dlg_strclone(nl_langinfo(items[n]));
82 	memset(shown, 0, sizeof(shown));
83     }
84 #endif
85     if (cached_days[n] == 0) {
86 	static const char *posix_days[MAX_DAYS] =
87 	{
88 	    "Sunday",
89 	    "Monday",
90 	    "Tuesday",
91 	    "Wednesday",
92 	    "Thursday",
93 	    "Friday",
94 	    "Saturday"
95 	};
96 	size_t limit = MON_WIDE - 1;
97 	char *value = dlg_strclone(posix_days[n]);
98 
99 	/*
100 	 * POSIX does not actually say what the length of an abbreviated name
101 	 * is.  Typically it is 2, which will fit into our layout.  That also
102 	 * happens to work with CJK entries as seen in glibc, which are a
103 	 * double-width cell.  For now (2016/01/26), handle too-long names only
104 	 * for POSIX values.
105 	 */
106 	if (strlen(value) > limit)
107 	    value[limit] = '\0';
108 	cached_days[n] = value;
109     }
110     if (!shown[n]) {
111 	DLG_TRACE(("# DAY(%d) = '%s'\n", n, cached_days[n]));
112 	shown[n] = TRUE;
113     }
114     return cached_days[n];
115 }
116 
117 static const char *
118 nameOfMonth(int n)
119 {
120     static bool shown[MAX_MONTHS];
121 
122     while (n < 0) {
123 	n += MAX_MONTHS;
124     }
125     n %= MAX_MONTHS;
126 #ifdef ENABLE_NLS
127     if (cached_months[n] == 0) {
128 	const nl_item items[MAX_MONTHS] =
129 	{
130 	    MON_1, MON_2, MON_3, MON_4, MON_5, MON_6,
131 	    MON_7, MON_8, MON_9, MON_10, MON_11, MON_12
132 	};
133 	cached_months[n] = dlg_strclone(nl_langinfo(items[n]));
134 	memset(shown, 0, sizeof(shown));
135     }
136 #endif
137     if (cached_months[n] == 0) {
138 	static const char *posix_mons[MAX_MONTHS] =
139 	{
140 	    "January",
141 	    "February",
142 	    "March",
143 	    "April",
144 	    "May",
145 	    "June",
146 	    "July",
147 	    "August",
148 	    "September",
149 	    "October",
150 	    "November",
151 	    "December"
152 	};
153 	cached_months[n] = dlg_strclone(posix_mons[n]);
154     }
155     if (!shown[n]) {
156 	DLG_TRACE(("# MON(%d) = '%s'\n", n, cached_months[n]));
157 	shown[n] = TRUE;
158     }
159     return cached_months[n];
160 }
161 
162 /*
163  * Algorithm for Gregorian calendar.
164  */
165 static int
166 isleap(int y)
167 {
168     return ((y % 4 == 0) &&
169 	    ((y % 100 != 0) ||
170 	     (y % 400 == 0))) ? 1 : 0;
171 }
172 
173 static void
174 adjust_year_month(int *year, int *month)
175 {
176     while (*month < 0) {
177 	*month += MAX_MONTHS;
178 	*year -= 1;
179     }
180     while (*month >= MAX_MONTHS) {
181 	*month -= MAX_MONTHS;
182 	*year += 1;
183     }
184 }
185 
186 static int
187 days_per_month(int year, int month)
188 {
189     static const int nominal[] =
190     {
191 	31, 28, 31, 30, 31, 30,
192 	31, 31, 30, 31, 30, 31
193     };
194     int result;
195 
196     adjust_year_month(&year, &month);
197     result = nominal[month];
198     if (month == 1)
199 	result += isleap(year);
200     return result;
201 }
202 
203 static int
204 days_in_month(struct tm *current, int offset /* -1, 0, 1 */ )
205 {
206     int year = current->tm_year + 1900;
207     int month = current->tm_mon + offset;
208 
209     adjust_year_month(&year, &month);
210     return days_per_month(year, month);
211 }
212 
213 static int
214 days_per_year(int year)
215 {
216     return (isleap(year) ? 366 : 365);
217 }
218 
219 static int
220 days_in_year(struct tm *current, int offset /* -1, 0, 1 */ )
221 {
222     return days_per_year(current->tm_year + 1900 + offset);
223 }
224 
225 /*
226  * Adapted from C FAQ
227  * "17.28: How can I find the day of the week given the date?"
228  * implementation by Tomohiko Sakamoto.
229  *
230  * d = day (0 to whatever)
231  * m = month (1 through 12)
232  * y = year (1752 and later, for Gregorian calendar)
233  */
234 static int
235 day_of_week(int y, int m, int d)
236 {
237     static int t[] =
238     {
239 	0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4
240     };
241     y -= (m < 3);
242     return (6 + (y + (y / 4) - (y / 100) + (y / 400) + t[m - 1] + d)) % MAX_DAYS;
243 }
244 
245 static int
246 day_in_year(int year, int month, int day)
247 {
248     int result = day;
249     while (--month >= 1)
250 	result += days_per_month(year, month);
251     return result;
252 }
253 
254 static int
255 iso_week(int year, int month, int day)
256 {
257     int week = 1;
258     int dow;
259     int new_year_dow;
260     int diy;
261     int new_years_eve_dow;
262     static const int thursday = 3;
263 
264     /* add the number weeks *between* date and newyear */
265     diy = day_in_year(year, month, day);
266     week += (diy - 1) / MAX_DAYS;
267 
268     /* 0 = Monday */
269     dow = day_of_week(year, month, day);
270     new_year_dow = day_of_week(year, 1, 1);
271 
272     /*
273      * If New Year falls on Friday, Saturday or Sunday, then New Years's week
274      * is the last week of the preceding year.  In that case subtract one week.
275      */
276     if (new_year_dow > thursday)
277 	--week;
278 
279     /* Add one week if there is a Sunday to Monday transition. */
280     if (dow - new_year_dow < 0)
281 	++week;
282 
283     /* Check if we are in the last week of the preceding year. */
284     if (week < 1) {
285 	week = iso_week(--year, 12, 31);
286     }
287 
288     /*
289      * If we are in the same week as New Year's eve, check if New Year's eve is
290      * in the first week of the next year.
291      */
292     new_years_eve_dow = (new_year_dow + 364 + isleap(year)) % MAX_DAYS;
293     if (365 + isleap(year) - diy < MAX_DAYS
294 	&& new_years_eve_dow >= dow
295 	&& new_years_eve_dow < thursday) {
296 	week = 1;
297     }
298     return week;
299 }
300 
301 static int *
302 getisoweeks(int year, int month)
303 {
304     static int result[10];
305     int windx = 0;
306     int day;
307     int dpm = days_per_month(year, month);
308 
309     for (day = 1; day <= dpm; day += MAX_DAYS)
310 	result[windx++] = iso_week(year, month, day);
311     /*
312      * Ensure that there is a week number associated with the last day of the
313      * month, e.g., in case the last day of the month falls before Thursday,
314      * so that we have to show the week-number for the beginning of the
315      * following month.
316      */
317     result[windx] = iso_week(year, month, dpm);
318     return result;
319 }
320 
321 static int
322 day_cell_number(struct tm *current)
323 {
324     int cell;
325     cell = current->tm_mday - ((6 + current->tm_mday - current->tm_wday) % MAX_DAYS);
326     if ((current->tm_mday - 1) % MAX_DAYS != current->tm_wday)
327 	cell += 6;
328     else
329 	cell--;
330     return cell;
331 }
332 
333 static int
334 next_or_previous(int key, int two_d)
335 {
336     int result = 0;
337 
338     switch (key) {
339     case DLGK_GRID_UP:
340 	result = two_d ? -MAX_DAYS : -1;
341 	break;
342     case DLGK_GRID_LEFT:
343 	result = -1;
344 	break;
345     case DLGK_GRID_DOWN:
346 	result = two_d ? MAX_DAYS : 1;
347 	break;
348     case DLGK_GRID_RIGHT:
349 	result = 1;
350 	break;
351     default:
352 	beep();
353 	break;
354     }
355     return result;
356 }
357 
358 /*
359  * Draw the day-of-month selection box
360  */
361 static int
362 draw_day(BOX * data, struct tm *current)
363 {
364     int cell_wide = MON_WIDE;
365     int y, x, this_x;
366     int save_y = 0, save_x = 0;
367     int day = current->tm_mday;
368     int mday;
369     int week = 0;
370     int windx = 0;
371     int *weeks = 0;
372     int last = days_in_month(current, 0);
373     int prev = days_in_month(current, -1);
374 
375     werase(data->window);
376     dlg_draw_box2(data->parent,
377 		  data->y - MARGIN, data->x - MARGIN,
378 		  data->height + (2 * MARGIN), data->width + (2 * MARGIN),
379 		  menubox_attr,
380 		  menubox_border_attr,
381 		  menubox_border2_attr);
382 
383     dlg_attrset(data->window, menubox_attr);	/* daynames headline */
384     for (x = 0; x < MAX_DAYS; x++) {
385 	mvwprintw(data->window,
386 		  0, (x + 1) * cell_wide, "%*.*s ",
387 		  cell_wide - 1,
388 		  cell_wide - 1,
389 		  nameOfDayOfWeek(x + data->week_start));
390     }
391 
392     mday = ((6 + current->tm_mday -
393 	     current->tm_wday +
394 	     data->week_start) % MAX_DAYS) - MAX_DAYS;
395     if (mday <= -MAX_DAYS)
396 	mday += MAX_DAYS;
397 
398     if (dialog_vars.iso_week) {
399 	weeks = getisoweeks(current->tm_year + 1900, current->tm_mon + 1);
400     } else {
401 	/* mday is now in the range -6 to 0. */
402 	week = (current->tm_yday + 6 + mday - current->tm_mday) / MAX_DAYS;
403     }
404 
405     for (y = 1; mday < last; y++) {
406 	dlg_attrset(data->window, menubox_attr);	/* weeknumbers headline */
407 	mvwprintw(data->window,
408 		  y, 0,
409 		  "%*d ",
410 		  cell_wide - 1,
411 		  weeks ? weeks[windx++] : ++week);
412 	for (x = 0; x < MAX_DAYS; x++) {
413 	    this_x = 1 + (x + 1) * cell_wide;
414 	    ++mday;
415 	    if (wmove(data->window, y, this_x) == ERR)
416 		continue;
417 	    dlg_attrset(data->window, item_attr);	/* not selected days */
418 	    if (mday == day) {
419 		dlg_attrset(data->window, item_selected_attr);	/* selected day */
420 		save_y = y;
421 		save_x = this_x;
422 	    }
423 	    if (mday > 0) {
424 		if (mday <= last) {
425 		    wprintw(data->window, "%*d", cell_wide - 2, mday);
426 		} else if (mday == day) {
427 		    wprintw(data->window, "%*d", cell_wide - 2, mday - last);
428 		}
429 	    } else if (mday == day) {
430 		wprintw(data->window, "%*d", cell_wide - 2, mday + prev);
431 	    }
432 	}
433 	wmove(data->window, save_y, save_x);
434     }
435     /* just draw arrows - scrollbar is unsuitable here */
436     dlg_draw_arrows(data->parent, TRUE, TRUE,
437 		    data->x + ARROWS_COL,
438 		    data->y - 1,
439 		    data->y + data->height);
440 
441     return 0;
442 }
443 
444 /*
445  * Draw the month-of-year selection box
446  */
447 static int
448 draw_month(BOX * data, struct tm *current)
449 {
450     int month;
451 
452     month = current->tm_mon + 1;
453 
454     dlg_attrset(data->parent, dialog_attr);	/* Headline "Month" */
455     (void) mvwprintw(data->parent, data->y - 2, data->x - 1, _("Month"));
456     dlg_draw_box2(data->parent,
457 		  data->y - 1, data->x - 1,
458 		  data->height + 2, data->width + 2,
459 		  menubox_attr,
460 		  menubox_border_attr,
461 		  menubox_border2_attr);
462     dlg_attrset(data->window, item_attr);	/* color the month selection */
463     mvwprintw(data->window, 0, 0, "%s", nameOfMonth(month - 1));
464     wmove(data->window, 0, 0);
465     return 0;
466 }
467 
468 /*
469  * Draw the year selection box
470  */
471 static int
472 draw_year(BOX * data, struct tm *current)
473 {
474     int year = current->tm_year + 1900;
475 
476     dlg_attrset(data->parent, dialog_attr);	/* Headline "Year" */
477     (void) mvwprintw(data->parent, data->y - 2, data->x - 1, _("Year"));
478     dlg_draw_box2(data->parent,
479 		  data->y - 1, data->x - 1,
480 		  data->height + 2, data->width + 2,
481 		  menubox_attr,
482 		  menubox_border_attr,
483 		  menubox_border2_attr);
484     dlg_attrset(data->window, item_attr);	/* color the year selection */
485     mvwprintw(data->window, 0, 0, "%4d", year);
486     wmove(data->window, 0, 0);
487     return 0;
488 }
489 
490 static int
491 init_object(BOX * data,
492 	    WINDOW *parent,
493 	    int x, int y,
494 	    int width, int height,
495 	    BOX_DRAW box_draw,
496 	    int key_offset,
497 	    int code)
498 {
499     data->parent = parent;
500     data->x = x;
501     data->y = y;
502     data->width = width;
503     data->height = height;
504     data->box_draw = box_draw;
505     data->week_start = key_offset;
506 
507     data->window = dlg_der_window(data->parent,
508 				  data->height, data->width,
509 				  data->y, data->x);
510     if (data->window == 0)
511 	return -1;
512 
513     dlg_mouse_setbase(getbegx(parent), getbegy(parent));
514     if (code == 'D') {
515 	dlg_mouse_mkbigregion(y + 1, x + MON_WIDE, height - 1, width - MON_WIDE,
516 			      KEY_MAX + key_offset, 1, MON_WIDE, 3);
517     } else {
518 	dlg_mouse_mkregion(y, x, height, width, code);
519     }
520 
521     return 0;
522 }
523 
524 #if defined(ENABLE_NLS) && defined(HAVE_NL_LANGINFO_1STDAY)
525 #elif defined(HAVE_DLG_GAUGE)
526 static int
527 read_locale_setting(const char *name, int which)
528 {
529     FILE *fp;
530     char command[80];
531     int result = -1;
532 
533     sprintf(command, "locale %s", name);
534     if ((fp = dlg_popen(command, "r")) != 0) {
535 	int count = 0;
536 	char buf[80];
537 
538 	while (fgets(buf, (int) sizeof(buf) - 1, fp) != 0) {
539 	    if (++count > which) {
540 		char *next = 0;
541 		long check = strtol(buf, &next, 0);
542 		if (next != 0 &&
543 		    next != buf &&
544 		    *next == '\n') {
545 		    result = (int) check;
546 		}
547 		break;
548 	    }
549 	}
550 	pclose(fp);
551     }
552     return result;
553 }
554 #endif
555 
556 static int
557 WeekStart(void)
558 {
559     int result = 0;
560     char *option = dialog_vars.week_start;
561     if (option != 0) {
562 	if (option[0]) {
563 	    char *next = 0;
564 	    long check = strtol(option, &next, 0);
565 	    if (next == 0 ||
566 		next == option ||
567 		*next != '\0') {
568 		if (!strcmp(option, "locale")) {
569 #if defined(ENABLE_NLS) && defined(HAVE_NL_LANGINFO_1STDAY)
570 		    /*
571 		     * glibc-specific.
572 		     */
573 		    int first_day = nl_langinfo(_NL_TIME_FIRST_WEEKDAY)[0];
574 		    char *basis_ptr = nl_langinfo(_NL_TIME_WEEK_1STDAY);
575 		    int basis_day = (int) (intptr_t) basis_ptr;
576 #elif defined(HAVE_DLG_GAUGE)
577 		    /*
578 		     * probably Linux-specific, but harmless otherwise.  When
579 		     * available, the locale program will return a single
580 		     * integer on one line.
581 		     */
582 		    int first_day = read_locale_setting("first_weekday", 0);
583 		    int basis_day = read_locale_setting("week-1stday", 0);
584 #endif
585 #if (defined(ENABLE_NLS) && defined(HAVE_NL_LANGINFO_1STDAY)) || defined(HAVE_DLG_GAUGE)
586 		    int week_1stday = -1;
587 		    if (basis_day == 19971130)
588 			week_1stday = 0;	/* Sun */
589 		    else if (basis_day == 19971201)
590 			week_1stday = 1;	/* Mon */
591 		    if (week_1stday >= 0) {
592 			result = first_day - week_1stday - 1;
593 		    }
594 #else
595 		    result = 0;	/* Sun */
596 #endif
597 		} else {
598 		    int day;
599 		    size_t eql = strlen(option);
600 		    for (day = 0; day < MAX_DAYS; ++day) {
601 			if (!strncmp(nameOfDayOfWeek(day), option, eql)) {
602 			    result = day;
603 			    break;
604 			}
605 		    }
606 		}
607 	    } else if (check < 0) {
608 		result = -1;
609 	    } else {
610 		result = (int) (check % MAX_DAYS);
611 	    }
612 	}
613     }
614     return result;
615 }
616 
617 static int
618 CleanupResult(int code, WINDOW *dialog, char *prompt, DIALOG_VARS * save_vars)
619 {
620     int n;
621 
622     if (dialog != 0)
623 	dlg_del_window(dialog);
624     dlg_mouse_free_regions();
625     if (prompt != 0)
626 	free(prompt);
627     dlg_restore_vars(save_vars);
628 
629     for (n = 0; n < MAX_DAYS; ++n) {
630 	free(cached_days[n]);
631 	cached_days[n] = 0;
632     }
633     for (n = 0; n < MAX_MONTHS; ++n) {
634 	free(cached_months[n]);
635 	cached_months[n] = 0;
636     }
637 
638     return code;
639 }
640 
641 static void
642 trace_date(struct tm *current, struct tm *old)
643 {
644     bool changed = (old == 0 ||
645 		    current->tm_mday != old->tm_mday ||
646 		    current->tm_mon != old->tm_mon ||
647 		    current->tm_year != old->tm_year);
648     if (changed) {
649 	DLG_TRACE(("# current %04d/%02d/%02d\n",
650 		   current->tm_year + 1900,
651 		   current->tm_mon + 1,
652 		   current->tm_mday));
653     } else {
654 	DLG_TRACE(("# current (unchanged)\n"));
655     }
656 }
657 
658 #define DrawObject(data) (data)->box_draw(data, &current)
659 
660 /*
661  * Display a dialog box for entering a date
662  */
663 int
664 dialog_calendar(const char *title,
665 		const char *subtitle,
666 		int height,
667 		int width,
668 		int day,
669 		int month,
670 		int year)
671 {
672     /* *INDENT-OFF* */
673     static DLG_KEYS_BINDING binding[] = {
674 	HELPKEY_BINDINGS,
675 	ENTERKEY_BINDINGS,
676 	TOGGLEKEY_BINDINGS,
677 	DLG_KEYS_DATA( DLGK_FIELD_NEXT, TAB ),
678 	DLG_KEYS_DATA( DLGK_FIELD_PREV, KEY_BTAB ),
679 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	'j' ),
680 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	DLGK_MOUSE(KEY_NPAGE) ),
681 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	KEY_DOWN ),
682 	DLG_KEYS_DATA( DLGK_GRID_DOWN,	KEY_NPAGE ),
683 	DLG_KEYS_DATA( DLGK_GRID_LEFT,	'-' ),
684 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  'h' ),
685 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  CHR_BACKSPACE ),
686 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  CHR_PREVIOUS ),
687 	DLG_KEYS_DATA( DLGK_GRID_LEFT,  KEY_LEFT ),
688 	DLG_KEYS_DATA( DLGK_GRID_RIGHT,	'+' ),
689 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, 'l' ),
690 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, CHR_NEXT ),
691 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_NEXT ),
692 	DLG_KEYS_DATA( DLGK_GRID_RIGHT, KEY_RIGHT ),
693 	DLG_KEYS_DATA( DLGK_GRID_UP,	'k' ),
694 	DLG_KEYS_DATA( DLGK_GRID_UP,	KEY_PPAGE ),
695 	DLG_KEYS_DATA( DLGK_GRID_UP,	KEY_PREVIOUS ),
696 	DLG_KEYS_DATA( DLGK_GRID_UP,	KEY_UP ),
697 	DLG_KEYS_DATA( DLGK_GRID_UP,  	DLGK_MOUSE(KEY_PPAGE) ),
698 	END_KEYS_BINDING
699     };
700     /* *INDENT-ON* */
701 
702 #ifdef KEY_RESIZE
703     int old_height = height;
704     int old_width = width;
705 #endif
706     BOX dy_box, mn_box, yr_box;
707     int fkey;
708     int key;
709     int step;
710     int button;
711     int result = DLG_EXIT_UNKNOWN;
712     int week_start;
713     WINDOW *dialog;
714     time_t now_time;
715     struct tm current;
716     int state = dlg_default_button();
717     const char **buttons = dlg_ok_labels();
718     char *prompt;
719     int mincols = MIN_WIDE;
720     char buffer[MAX_LEN];
721     DIALOG_VARS save_vars;
722 
723     DLG_TRACE(("# calendar args:\n"));
724     DLG_TRACE2S("title", title);
725     DLG_TRACE2S("message", subtitle);
726     DLG_TRACE2N("height", height);
727     DLG_TRACE2N("width", width);
728     DLG_TRACE2N("day", day);
729     DLG_TRACE2N("month", month);
730     DLG_TRACE2N("year", year);
731 
732     dlg_save_vars(&save_vars);
733     dialog_vars.separate_output = TRUE;
734 
735     dlg_does_output();
736 
737     /*
738      * Unless overridden, the current time/date is our starting point.
739      */
740     now_time = time((time_t *) 0);
741     current = *localtime(&now_time);
742 
743 #ifdef HAVE_MKTIME
744     current.tm_isdst = -1;
745     if (year >= 1900) {
746 	current.tm_year = year - 1900;
747     }
748     if (month >= 1) {
749 	current.tm_mon = month - 1;
750     }
751     if (day > 0 && day <= days_per_month(current.tm_year + 1900,
752 					 current.tm_mon)) {
753 	current.tm_mday = day;
754     }
755     now_time = mktime(&current);
756 #else
757     if (day < 0)
758 	day = current.tm_mday;
759     if (month < 0)
760 	month = current.tm_mon + 1;
761     if (year < 0)
762 	year = current.tm_year + 1900;
763 
764     /* compute a struct tm that matches the day/month/year parameters */
765     if (((year -= 1900) > 0) && (year < 200)) {
766 	/* ugly, but I'd like to run this on older machines w/o mktime -TD */
767 	for (;;) {
768 	    if (year > current.tm_year) {
769 		now_time += ONE_DAY * days_in_year(&current, 0);
770 	    } else if (year < current.tm_year) {
771 		now_time -= ONE_DAY * days_in_year(&current, -1);
772 	    } else if (month > current.tm_mon + 1) {
773 		now_time += ONE_DAY * days_in_month(&current, 0);
774 	    } else if (month < current.tm_mon + 1) {
775 		now_time -= ONE_DAY * days_in_month(&current, -1);
776 	    } else if (day > current.tm_mday) {
777 		now_time += ONE_DAY;
778 	    } else if (day < current.tm_mday) {
779 		now_time -= ONE_DAY;
780 	    } else {
781 		break;
782 	    }
783 	    current = *localtime(&now_time);
784 	}
785     }
786 #endif
787 
788     dlg_button_layout(buttons, &mincols);
789 
790 #ifdef KEY_RESIZE
791   retry:
792 #endif
793 
794     prompt = dlg_strclone(subtitle);
795     dlg_auto_size(title, prompt, &height, &width, 0, mincols);
796 
797     height += MIN_HIGH - 1;
798     dlg_print_size(height, width);
799     dlg_ctl_size(height, width);
800 
801     dialog = dlg_new_window(height, width,
802 			    dlg_box_y_ordinate(height),
803 			    dlg_box_x_ordinate(width));
804     dlg_register_window(dialog, "calendar", binding);
805     dlg_register_buttons(dialog, "calendar", buttons);
806 
807     /* mainbox */
808     dlg_draw_box2(dialog, 0, 0, height, width, dialog_attr, border_attr, border2_attr);
809     dlg_draw_bottom_box2(dialog, border_attr, border2_attr, dialog_attr);
810     dlg_draw_title(dialog, title);
811 
812     dlg_attrset(dialog, dialog_attr);	/* text mainbox */
813     dlg_print_autowrap(dialog, prompt, height, width);
814 
815     /* compute positions of day, month and year boxes */
816     memset(&dy_box, 0, sizeof(dy_box));
817     memset(&mn_box, 0, sizeof(mn_box));
818     memset(&yr_box, 0, sizeof(yr_box));
819 
820     if ((week_start = WeekStart()) < 0 ||
821 	init_object(&dy_box,
822 		    dialog,
823 		    (width - DAY_WIDE) / 2,
824 		    1 + (height - (DAY_HIGH + BTN_HIGH + (5 * MARGIN))),
825 		    DAY_WIDE,
826 		    DAY_HIGH + 1,
827 		    draw_day,
828 		    week_start,
829 		    'D') < 0 ||
830 	((dy_box.week_start = WeekStart()) < 0) ||
831 	DrawObject(&dy_box) < 0) {
832 	return CleanupResult(DLG_EXIT_ERROR, dialog, prompt, &save_vars);
833     }
834 
835     if (init_object(&mn_box,
836 		    dialog,
837 		    dy_box.x,
838 		    dy_box.y - (HDR_HIGH + 2 * MARGIN),
839 		    (DAY_WIDE / 2) - MARGIN,
840 		    HDR_HIGH,
841 		    draw_month,
842 		    0,
843 		    'M') < 0
844 	|| DrawObject(&mn_box) < 0) {
845 	return CleanupResult(DLG_EXIT_ERROR, dialog, prompt, &save_vars);
846     }
847 
848     if (init_object(&yr_box,
849 		    dialog,
850 		    dy_box.x + mn_box.width + 2,
851 		    mn_box.y,
852 		    mn_box.width,
853 		    mn_box.height,
854 		    draw_year,
855 		    0,
856 		    'Y') < 0
857 	|| DrawObject(&yr_box) < 0) {
858 	return CleanupResult(DLG_EXIT_ERROR, dialog, prompt, &save_vars);
859     }
860 
861     dlg_trace_win(dialog);
862     while (result == DLG_EXIT_UNKNOWN) {
863 	int key2;
864 	BOX *obj = (state == sDAY ? &dy_box
865 		    : (state == sMONTH ? &mn_box :
866 		       (state == sYEAR ? &yr_box : 0)));
867 
868 	button = (state < 0) ? 0 : state;
869 	dlg_draw_buttons(dialog, height - 2, 0, buttons, button, FALSE, width);
870 	if (obj != 0)
871 	    dlg_set_focus(dialog, obj->window);
872 
873 	key = dlg_mouse_wgetch(dialog, &fkey);
874 	if (dlg_result_key(key, fkey, &result)) {
875 	    if (!dlg_button_key(result, &button, &key, &fkey))
876 		break;
877 	}
878 #define Mouse2Key(key) (key - M_EVENT)
879 	if (fkey && (key >= DLGK_MOUSE(KEY_MIN) && key <= DLGK_MOUSE(KEY_MAX))) {
880 	    key = dlg_lookup_key(dialog, Mouse2Key(key), &fkey);
881 	}
882 
883 	if ((key2 = dlg_char_to_button(key, buttons)) >= 0) {
884 	    result = key2;
885 	} else if (fkey) {
886 	    /* handle function-keys */
887 	    switch (key) {
888 	    case DLGK_MOUSE('D'):
889 		state = sDAY;
890 		break;
891 	    case DLGK_MOUSE('M'):
892 		state = sMONTH;
893 		break;
894 	    case DLGK_MOUSE('Y'):
895 		state = sYEAR;
896 		break;
897 	    case DLGK_TOGGLE:
898 	    case DLGK_ENTER:
899 		result = dlg_enter_buttoncode(button);
900 		break;
901 	    case DLGK_LEAVE:
902 		result = dlg_ok_buttoncode(button);
903 		break;
904 	    case DLGK_FIELD_PREV:
905 		state = dlg_prev_ok_buttonindex(state, sMONTH);
906 		break;
907 	    case DLGK_FIELD_NEXT:
908 		state = dlg_next_ok_buttonindex(state, sMONTH);
909 		break;
910 #ifdef KEY_RESIZE
911 	    case KEY_RESIZE:
912 		dlg_will_resize(dialog);
913 		/* reset data */
914 		height = old_height;
915 		width = old_width;
916 		free(prompt);
917 		_dlg_resize_cleanup(dialog);
918 		/* repaint */
919 		goto retry;
920 #endif
921 	    default:
922 		step = 0;
923 		key2 = -1;
924 		if (is_DLGK_MOUSE(key)) {
925 		    if ((key2 = dlg_ok_buttoncode(Mouse2Key(key))) >= 0) {
926 			result = key2;
927 			break;
928 		    } else if (key >= DLGK_MOUSE(KEY_MAX)) {
929 			state = sDAY;
930 			obj = &dy_box;
931 			key2 = 1;
932 			step = (key
933 				- DLGK_MOUSE(KEY_MAX)
934 				- day_cell_number(&current));
935 			DLG_TRACE(("# mouseclick decoded %d\n", step));
936 		    }
937 		}
938 		if (obj != 0) {
939 		    if (key2 < 0) {
940 			step = next_or_previous(key, (obj == &dy_box));
941 		    }
942 		    if (step != 0) {
943 			struct tm old = current;
944 
945 			/* see comment regarding mktime -TD */
946 			if (obj == &dy_box) {
947 			    now_time += ONE_DAY * step;
948 			} else if (obj == &mn_box) {
949 			    if (step > 0)
950 				now_time += ONE_DAY *
951 				    days_in_month(&current, 0);
952 			    else
953 				now_time -= ONE_DAY *
954 				    days_in_month(&current, -1);
955 			} else if (obj == &yr_box) {
956 			    if (step > 0)
957 				now_time += (ONE_DAY
958 					     * days_in_year(&current, 0));
959 			    else
960 				now_time -= (ONE_DAY
961 					     * days_in_year(&current, -1));
962 			}
963 
964 			current = *localtime(&now_time);
965 
966 			trace_date(&current, &old);
967 			if (obj != &dy_box
968 			    && (current.tm_mday != old.tm_mday
969 				|| current.tm_mon != old.tm_mon
970 				|| current.tm_year != old.tm_year))
971 			    DrawObject(&dy_box);
972 			if (obj != &mn_box && current.tm_mon != old.tm_mon)
973 			    DrawObject(&mn_box);
974 			if (obj != &yr_box && current.tm_year != old.tm_year)
975 			    DrawObject(&yr_box);
976 			(void) DrawObject(obj);
977 		    }
978 		} else if (state >= 0) {
979 		    if (next_or_previous(key, FALSE) < 0)
980 			state = dlg_prev_ok_buttonindex(state, sMONTH);
981 		    else if (next_or_previous(key, FALSE) > 0)
982 			state = dlg_next_ok_buttonindex(state, sMONTH);
983 		}
984 		break;
985 	    }
986 	}
987     }
988 
989 #define DefaultFormat(dst, src) \
990 	sprintf(dst, "%02d/%02d/%0d", \
991 		src.tm_mday, src.tm_mon + 1, src.tm_year + 1900)
992 #ifdef HAVE_STRFTIME
993     if (dialog_vars.date_format != 0) {
994 	size_t used = strftime(buffer,
995 			       sizeof(buffer) - 1,
996 			       dialog_vars.date_format,
997 			       &current);
998 	if (used == 0 || *buffer == '\0')
999 	    DefaultFormat(buffer, current);
1000     } else
1001 #endif
1002 	DefaultFormat(buffer, current);
1003 
1004     dlg_add_result(buffer);
1005     AddLastKey();
1006 
1007     return CleanupResult(result, dialog, prompt, &save_vars);
1008 }
1009