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