xref: /freebsd-14-stable/contrib/bsddialog/lib/calendarbox.c (revision 84823cc70824c8d842f503d8c2e6d7b0c2d95b61)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2022 Alfonso Sabato Siciliano
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  */
27 
28 #include <sys/param.h>
29 
30 #include <curses.h>
31 #include <stdlib.h>
32 #include <string.h>
33 
34 #include "bsddialog.h"
35 #include "bsddialog_theme.h"
36 #include "lib_util.h"
37 
38 #define MINHCAL   13
39 #define MINWCAL   36 /* 34 calendar, 1 + 1 margins */
40 #define MINYEAR   1900
41 #define MAXYEAR   999999999
42 
month_days(int yy,int mm)43 static int month_days(int yy, int mm)
44 {
45 	int days;
46 
47 	if (mm == 2)
48 		days = ISLEAP(yy) ? 29 : 28;
49 	else if (mm == 4 || mm == 6 || mm == 9 || mm == 11)
50 		days = 30;
51 	else
52 		days = 31;
53 
54 	return (days);
55 }
56 
57 enum operation {
58 	UP_DAY,
59 	DOWN_DAY,
60 	LEFT_DAY,
61 	RIGHT_DAY,
62 	UP_MONTH,
63 	DOWN_MONTH,
64 	UP_YEAR,
65 	DOWN_YEAR
66 };
67 
datectl(enum operation op,int * yy,int * mm,int * dd)68 static void datectl(enum operation op, int *yy, int *mm, int *dd)
69 {
70 	int ndays;
71 
72 	ndays = month_days(*yy, *mm);
73 
74 	switch (op) {
75 	case UP_DAY:
76 		if (*dd > 7)
77 			*dd -= 7;
78 		else {
79 			if (*mm == 1) {
80 				*yy -= 1;
81 				*mm = 12;
82 			} else
83 				*mm -= 1;
84 			ndays = month_days(*yy, *mm);
85 			*dd = ndays - abs(7 - *dd);
86 		}
87 		break;
88 	case DOWN_DAY:
89 		if (*dd + 7 < ndays)
90 			*dd += 7;
91 		else {
92 			if (*mm == 12) {
93 				*yy += 1;
94 				*mm = 1;
95 			} else
96 				*mm += 1;
97 			*dd = *dd + 7 - ndays;
98 		}
99 		break;
100 	case LEFT_DAY:
101 		if (*dd > 1)
102 			*dd -= 1;
103 		else {
104 			if (*mm == 1) {
105 				*yy -= 1;
106 				*mm = 12;
107 			} else
108 				*mm -= 1;
109 			*dd = month_days(*yy, *mm);
110 		}
111 		break;
112 	case RIGHT_DAY:
113 		if (*dd < ndays)
114 			*dd += 1;
115 		else {
116 			if (*mm == 12) {
117 				*yy += 1;
118 				*mm = 1;
119 			} else
120 				*mm += 1;
121 			*dd = 1;
122 		}
123 		break;
124 	case UP_MONTH:
125 		if (*mm == 1) {
126 			*mm = 12;
127 			*yy -= 1;
128 		} else
129 			*mm -= 1;
130 		ndays = month_days(*yy, *mm);
131 		if (*dd > ndays)
132 			*dd = ndays;
133 		break;
134 	case DOWN_MONTH:
135 		if (*mm == 12) {
136 			*mm = 1;
137 			*yy += 1;
138 		} else
139 			*mm += 1;
140 		ndays = month_days(*yy, *mm);
141 		if (*dd > ndays)
142 			*dd = ndays;
143 		break;
144 	case UP_YEAR:
145 		*yy -= 1;
146 		ndays = month_days(*yy, *mm);
147 		if (*dd > ndays)
148 			*dd = ndays;
149 		break;
150 	case DOWN_YEAR:
151 		*yy += 1;
152 		ndays = month_days(*yy, *mm);
153 		if (*dd > ndays)
154 			*dd = ndays;
155 		break;
156 	}
157 
158 	if (*yy < MINYEAR) {
159 		*yy = MINYEAR;
160 		*mm = 1;
161 		*dd = 1;
162 	}
163 	if (*yy > MAXYEAR) {
164 		*yy = MAXYEAR;
165 		*mm = 12;
166 		*dd = 31;
167 	}
168 }
169 
week_day(int yy,int mm,int dd)170 static int week_day(int yy, int mm, int dd)
171 {
172 	int wd;
173 
174 	dd += mm < 3 ? yy-- : yy - 2;
175 	wd = 23*mm/9 + dd + 4 + yy/4 - yy/100 + yy/400;
176 	wd %= 7;
177 
178 	return (wd);
179 }
180 
181 static void
print_calendar(struct bsddialog_conf * conf,WINDOW * win,int yy,int mm,int dd,bool active)182 print_calendar(struct bsddialog_conf *conf, WINDOW *win, int yy, int mm, int dd,
183     bool active)
184 {
185 	int ndays, i, y, x, wd, h, w;
186 
187 	getmaxyx(win, h, w);
188 	wclear(win);
189 	draw_borders(conf, win, h, w, RAISED);
190 	if (active) {
191 		wattron(win, t.dialog.arrowcolor);
192 		mvwhline(win, 0, 15, conf->ascii_lines ? '^' : ACS_UARROW, 4);
193 		mvwhline(win, h-1, 15, conf->ascii_lines ? 'v' : ACS_DARROW, 4);
194 		mvwvline(win, 3, 0, conf->ascii_lines ? '<' : ACS_LARROW, 3);
195 		mvwvline(win, 3, w-1, conf->ascii_lines ? '>' : ACS_RARROW, 3);
196 		wattroff(win, t.dialog.arrowcolor);
197 	}
198 
199 	mvwaddstr(win, 1, 5, "Sun Mon Tue Wed Thu Fri Sat");
200 	ndays = month_days(yy, mm);
201 	y = 2;
202 	wd = week_day(yy, mm, 1);
203 	for (i = 1; i <= ndays; i++) {
204 		x = 5 + (4 * wd); /* x has to be 6 with week number */
205 		wmove(win, y, x);
206 		mvwprintw(win, y, x, "%2d", i);
207 		if (i == dd) {
208 			wattron(win, t.menu.f_namecolor);
209 			mvwprintw(win, y, x, "%2d", i);
210 			wattroff(win, t.menu.f_namecolor);
211 		}
212 		wd++;
213 		if (wd > 6) {
214 			wd = 0;
215 			y++;
216 		}
217 	}
218 
219 	wrefresh(win);
220 }
221 
222 static void
drawsquare(struct bsddialog_conf * conf,WINDOW * win,const char * fmt,const void * value,bool focus)223 drawsquare(struct bsddialog_conf *conf, WINDOW *win, const char *fmt,
224     const void *value, bool focus)
225 {
226 	int h, w;
227 
228 	getmaxyx(win, h, w);
229 	draw_borders(conf, win, h, w, RAISED);
230 	if (focus) {
231 		wattron(win, t.dialog.arrowcolor);
232 		mvwhline(win, 0, 7, conf->ascii_lines ? '^' : ACS_UARROW, 3);
233 		mvwhline(win, 2, 7, conf->ascii_lines ? 'v' : ACS_DARROW, 3);
234 		wattroff(win, t.dialog.arrowcolor);
235 	}
236 
237 	if (focus)
238 		wattron(win, t.menu.f_namecolor);
239 	if (strchr(fmt, 's') != NULL)
240 		mvwprintw(win, 1, 1, fmt, (const char*)value);
241 	else
242 		mvwprintw(win, 1, 1, fmt, *((const int*)value));
243 	if (focus)
244 		wattroff(win, t.menu.f_namecolor);
245 
246 	wrefresh(win);
247 }
248 
249 static int
calendar_autosize(struct bsddialog_conf * conf,int rows,int cols,int * h,int * w,const char * text,struct buttons bs)250 calendar_autosize(struct bsddialog_conf *conf, int rows, int cols, int *h,
251     int *w, const char *text, struct buttons bs)
252 {
253 	int htext, wtext;
254 
255 	if (cols == BSDDIALOG_AUTOSIZE || rows == BSDDIALOG_AUTOSIZE) {
256 		if (text_size(conf, rows, cols, text, &bs, MINHCAL, MINWCAL,
257 		    &htext, &wtext) != 0)
258 			return (BSDDIALOG_ERROR);
259 	}
260 
261 	if (cols == BSDDIALOG_AUTOSIZE)
262 		*w = widget_min_width(conf, wtext, MINWCAL, &bs);
263 
264 	if (rows == BSDDIALOG_AUTOSIZE)
265 		*h = widget_min_height(conf, htext, MINHCAL, true);
266 
267 	return (0);
268 }
269 
calendar_checksize(int rows,int cols,struct buttons bs)270 static int calendar_checksize(int rows, int cols, struct buttons bs)
271 {
272 	int mincols;
273 
274 	mincols = MAX(MINWCAL, buttons_min_width(bs));
275 	mincols += VBORDERS;
276 
277 	if (cols < mincols)
278 		RETURN_ERROR("Few cols for this calendar (at least 38)");
279 
280 	if (rows < MINHCAL + 2 + 2) /* 2 buttons + 2 borders */
281 		RETURN_ERROR("Few rows for calendar (at least 17)");
282 
283 	return (0);
284 }
285 
286 int
bsddialog_calendar(struct bsddialog_conf * conf,const char * text,int rows,int cols,unsigned int * yy,unsigned int * mm,unsigned int * dd)287 bsddialog_calendar(struct bsddialog_conf *conf, const char *text, int rows,
288     int cols, unsigned int *yy, unsigned int *mm, unsigned int *dd)
289 {
290 	bool loop, focusbuttons;
291 	int retval, y, x, h, w, sel, ycal, xcal, year, month, day;
292 	wint_t input;
293 	WINDOW *widget, *textpad, *shadow, *yearwin, *monthwin, *daywin;
294 	struct buttons bs;
295 	const char *m[12] = {
296 		"January", "February", "March", "April", "May", "June", "July",
297 		"August", "September", "October", "November", "December"
298 	};
299 
300 	if (yy == NULL || mm == NULL || dd == NULL)
301 		RETURN_ERROR("yy / mm / dd cannot be NULL");
302 
303 	year = *yy > MAXYEAR ? MAXYEAR : *yy;
304 	if (year < MINYEAR)
305 		year = MINYEAR;
306 	month = *mm > 12 ? 12 : *mm;
307 	if (month == 0)
308 		month = 1;
309 	day = *dd == 0 ? 1 : *dd;
310 	if(day > month_days(year, month))
311 		day = month_days(year, month);
312 
313 	get_buttons(conf, &bs, BUTTON_OK_LABEL, BUTTON_CANCEL_LABEL);
314 
315 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
316 		return (BSDDIALOG_ERROR);
317 	if (calendar_autosize(conf, rows, cols, &h, &w, text, bs) != 0)
318 		return (BSDDIALOG_ERROR);
319 	if (calendar_checksize(h, w, bs) != 0)
320 		return (BSDDIALOG_ERROR);
321 	if (set_widget_position(conf, &y, &x, h, w) != 0)
322 		return (BSDDIALOG_ERROR);
323 
324 	if (new_dialog(conf, &shadow, &widget, y, x, h, w, &textpad, text, &bs,
325 	    true) != 0)
326 		return (BSDDIALOG_ERROR);
327 
328 	pnoutrefresh(textpad, 0, 0, y+1, x+2, y+h-17, x+w-2);
329 	doupdate();
330 
331 	ycal = y + h - 15;
332 	xcal = x + w/2 - 17;
333 	mvwaddstr(widget, h - 16, w/2 - 17, "Month");
334 	monthwin = new_boxed_window(conf, ycal, xcal, 3, 17, RAISED);
335 	mvwaddstr(widget, h - 16, w/2, "Year");
336 	yearwin = new_boxed_window(conf, ycal, xcal + 17, 3, 17, RAISED);
337 	daywin = new_boxed_window(conf, ycal + 3, xcal, 9, 34, RAISED);
338 
339 	wrefresh(widget);
340 
341 	sel = -1;
342 	loop = focusbuttons = true;
343 	while (loop) {
344 		drawsquare(conf, monthwin, "%15s", m[month - 1], sel == 0);
345 		drawsquare(conf, yearwin, "%15d", &year, sel == 1);
346 		print_calendar(conf, daywin, year, month, day, sel == 2);
347 
348 		if (get_wch(&input) == ERR)
349 			continue;
350 		switch(input) {
351 		case KEY_ENTER:
352 		case 10: /* Enter */
353 			if (focusbuttons || conf->button.always_active) {
354 				retval = bs.value[bs.curr];
355 				loop = false;
356 			}
357 			break;
358 		case 27: /* Esc */
359 			if (conf->key.enable_esc) {
360 				retval = BSDDIALOG_ESC;
361 				loop = false;
362 			}
363 			break;
364 		case '\t': /* TAB */
365 			if (focusbuttons) {
366 				bs.curr++;
367 				if (bs.curr >= (int)bs.nbuttons) {
368 					focusbuttons = false;
369 					sel = 0;
370 					bs.curr = conf->button.always_active ?
371 					    0 : -1;
372 				}
373 			} else {
374 				sel++;
375 				if (sel > 2) {
376 					focusbuttons = true;
377 					sel = -1;
378 					bs.curr = 0;
379 				}
380 			}
381 			draw_buttons(widget, bs, true);
382 			wrefresh(widget);
383 			break;
384 		case KEY_RIGHT:
385 			if (focusbuttons) {
386 				bs.curr++;
387 				if (bs.curr >= (int)bs.nbuttons) {
388 					focusbuttons = false;
389 					sel = 0;
390 					bs.curr = conf->button.always_active ?
391 					    0 : -1;
392 				}
393 			} else if (sel == 2) {
394 				datectl(RIGHT_DAY, &year, &month, &day);
395 			} else { /* Month or Year*/
396 				sel++;
397 			}
398 			draw_buttons(widget, bs, true);
399 			wrefresh(widget);
400 			break;
401 		case KEY_LEFT:
402 			if (focusbuttons) {
403 				bs.curr--;
404 				if (bs.curr < 0) {
405 					focusbuttons = false;
406 					sel = 2;
407 					bs.curr = conf->button.always_active ?
408 					    0 : -1;
409 				}
410 			} else if (sel == 2) {
411 				datectl(LEFT_DAY, &year, &month, &day);
412 			} else if (sel == 1) {
413 				sel = 0;
414 			} else { /* sel = 0, Month */
415 				focusbuttons = true;
416 				sel = -1;
417 				bs.curr = 0;
418 			}
419 			draw_buttons(widget, bs, true);
420 			wrefresh(widget);
421 			break;
422 		case KEY_UP:
423 			if (focusbuttons) {
424 				sel = 2;
425 				focusbuttons = false;
426 				bs.curr = conf->button.always_active ? 0 : -1;
427 				draw_buttons(widget, bs, true);
428 				wrefresh(widget);
429 			} else if (sel == 0) {
430 				datectl(UP_MONTH, &year, &month, &day);
431 			} else if (sel == 1) {
432 				datectl(UP_YEAR, &year, &month, &day);
433 			} else { /* sel = 2 */
434 				datectl(UP_DAY, &year, &month, &day);
435 			}
436 			break;
437 		case KEY_DOWN:
438 			if (focusbuttons) {
439 				break;
440 			} else if (sel == 0) {
441 				datectl(DOWN_MONTH, &year, &month, &day);
442 			} else if (sel == 1) {
443 				datectl(DOWN_YEAR, &year, &month, &day);
444 			} else { /* sel = 2 */
445 				datectl(DOWN_DAY, &year, &month, &day);
446 			}
447 			break;
448 		case KEY_HOME:
449 			datectl(UP_MONTH, &year, &month, &day);
450 			break;
451 		case KEY_END:
452 			datectl(DOWN_MONTH, &year, &month, &day);
453 			break;
454 		case KEY_PPAGE:
455 			datectl(UP_YEAR, &year, &month, &day);
456 			break;
457 		case KEY_NPAGE:
458 			datectl(DOWN_YEAR, &year, &month, &day);
459 			break;
460 		case KEY_F(1):
461 			if (conf->key.f1_file == NULL &&
462 			    conf->key.f1_message == NULL)
463 				break;
464 			if (f1help(conf) != 0)
465 				return (BSDDIALOG_ERROR);
466 			/* No break, screen size can change */
467 		case KEY_RESIZE:
468 			/* Important for decreasing screen */
469 			hide_widget(y, x, h, w, conf->shadow);
470 			refresh();
471 
472 			if (set_widget_size(conf, rows, cols, &h, &w) != 0)
473 				return (BSDDIALOG_ERROR);
474 			if (calendar_autosize(conf, rows, cols, &h, &w, text,
475 			    bs) != 0)
476 				return (BSDDIALOG_ERROR);
477 			if (calendar_checksize(h, w, bs) != 0)
478 				return (BSDDIALOG_ERROR);
479 			if (set_widget_position(conf, &y, &x, h, w) != 0)
480 				return (BSDDIALOG_ERROR);
481 
482 			if (update_dialog(conf, shadow, widget, y, x, h, w,
483 			    textpad, text, &bs, true) != 0)
484 				return (BSDDIALOG_ERROR);
485 			pnoutrefresh(textpad, 0, 0, y+1, x+2, y+h-17, x+w-2);
486 			doupdate();
487 
488 			ycal = y + h - 15;
489 			xcal = x + w/2 - 17;
490 			mvwaddstr(widget, h - 16, w/2 - 17, "Month");
491 			mvwin(monthwin, ycal, xcal);
492 			mvwaddstr(widget, h - 16, w/2, "Year");
493 			mvwin(yearwin, ycal, xcal + 17);
494 			mvwin(daywin, ycal + 3, xcal);
495 			wrefresh(widget);
496 
497 			/* Important to avoid grey lines expanding screen */
498 			refresh();
499 			break;
500 		default:
501 			if (shortcut_buttons(input, &bs)) {
502 				retval = bs.value[bs.curr];
503 				loop = false;
504 			}
505 		}
506 	}
507 
508 	if (retval == BSDDIALOG_OK) {
509 		*yy = year;
510 		*mm = month;
511 		*dd = day;
512 	}
513 
514 	delwin(yearwin);
515 	delwin(monthwin);
516 	delwin(daywin);
517 	end_dialog(conf, shadow, widget, textpad);
518 
519 	return (retval);
520 }