xref: /freebsd/contrib/bsddialog/lib/timebox.c (revision 66df505066f51e6d8411b966765d828817f88971)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2021 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 #ifdef PORTNCURSES
31 #include <ncurses/ncurses.h>
32 #else
33 #include <ncurses.h>
34 #endif
35 
36 #include <ctype.h>
37 #include <string.h>
38 
39 #include "bsddialog.h"
40 #include "lib_util.h"
41 #include "bsddialog_theme.h"
42 
43 #define MINWDATE 25 /* 23 wins + 2 VBORDERS */
44 #define MINWTIME 16 /*14 wins + 2 VBORDERS */
45 #define MINHEIGHT 8 /* 2 for text */
46 
47 /* "Time": timebox - datebox */
48 
49 extern struct bsddialog_theme t;
50 
51 static int
52 datetime_autosize(struct bsddialog_conf *conf, int rows, int cols, int *h,
53     int *w, int minw, char *text, struct buttons bs)
54 {
55 	int maxword, maxline, nlines, line;
56 
57 	if (get_text_properties(conf, text, &maxword, &maxline, &nlines) != 0)
58 		return BSDDIALOG_ERROR;
59 
60 	if (cols == BSDDIALOG_AUTOSIZE) {
61 		*w = VBORDERS;
62 		/* buttons size */
63 		*w += bs.nbuttons * bs.sizebutton;
64 		*w += bs.nbuttons > 0 ? (bs.nbuttons-1) * t.button.space : 0;
65 		/* text size */
66 		line = maxline + VBORDERS + t.text.hmargin * 2;
67 		line = MAX(line, (int) (maxword + VBORDERS + t.text.hmargin * 2));
68 		*w = MAX(*w, line);
69 		/* date windows */
70 		*w = MAX(*w, minw);
71 		/* conf.auto_minwidth */
72 		*w = MAX(*w, (int)conf->auto_minwidth);
73 		/* avoid terminal overflow */
74 		*w = MIN(*w, widget_max_width(conf) -1);
75 	}
76 
77 	if (rows == BSDDIALOG_AUTOSIZE) {
78 		*h = MINHEIGHT;
79 		if (maxword > 0)
80 			*h += MAX(nlines, (int)(*w / GET_ASPECT_RATIO(conf)));
81 		/* conf.auto_minheight */
82 		*h = MAX(*h, (int)conf->auto_minheight);
83 		/* avoid terminal overflow */
84 		*h = MIN(*h, widget_max_height(conf) -1);
85 	}
86 
87 	return 0;
88 }
89 
90 static int
91 datetime_checksize(int rows, int cols, char *text, int minw, struct buttons bs)
92 {
93 	int mincols;
94 
95 	mincols = VBORDERS;
96 	mincols += bs.nbuttons * bs.sizebutton;
97 	mincols += bs.nbuttons > 0 ? (bs.nbuttons-1) * t.button.space : 0;
98 	mincols = MAX(minw, mincols);
99 
100 	if (cols < mincols)
101 		RETURN_ERROR("Few cols for this timebox/datebox");
102 
103 	if (rows < MINHEIGHT + (strlen(text) > 0 ? 1 : 0))
104 		RETURN_ERROR("Few rows for this timebox/datebox");
105 
106 	return 0;
107 }
108 
109 int bsddialog_timebox(struct bsddialog_conf *conf, char* text, int rows, int cols,
110     unsigned int *hh, unsigned int *mm, unsigned int *ss)
111 {
112 	WINDOW *widget, *textpad, *shadow;
113 	int i, input, output, y, x, h, w, sel, htextpad;
114 	struct buttons bs;
115 	bool loop;
116 	struct myclockstruct {
117 		unsigned int max;
118 		unsigned int value;
119 		WINDOW *win;
120 	};
121 
122 	if (hh == NULL || mm == NULL || ss == NULL)
123 		RETURN_ERROR("hh / mm / ss cannot be NULL");
124 
125 	struct myclockstruct c[3] = {
126 		{23, *hh, NULL},
127 		{59, *mm, NULL},
128 		{59, *ss, NULL}
129 	};
130 
131 	for (i = 0 ; i < 3; i++) {
132 		if (c[i].value > c[i].max)
133 			c[i].value = c[i].max;
134 	}
135 
136 	get_buttons(conf, &bs, BUTTONLABEL(ok_label), BUTTONLABEL(extra_label),
137 	    BUTTONLABEL(cancel_label), BUTTONLABEL(help_label));
138 
139 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
140 		return BSDDIALOG_ERROR;
141 	if (datetime_autosize(conf, rows, cols, &h, &w, MINWTIME, text, bs) != 0)
142 		return BSDDIALOG_ERROR;
143 	if (datetime_checksize(h, w, text, MINWTIME, bs) != 0)
144 		return BSDDIALOG_ERROR;
145 	if (set_widget_position(conf, &y, &x, h, w) != 0)
146 		return BSDDIALOG_ERROR;
147 
148 	if (new_widget_withtextpad(conf, &shadow, &widget, y, x, h, w, RAISED,
149 	    &textpad, &htextpad, text, true) != 0)
150 		return BSDDIALOG_ERROR;
151 
152 	draw_buttons(widget, h-2, w, bs, true);
153 
154 	wrefresh(widget);
155 
156 	prefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
157 
158 	c[0].win = new_boxed_window(conf, y + h - 6, x + w/2 - 7, 3, 4, LOWERED);
159 	mvwaddch(widget, h - 5, w/2 - 3, ':');
160 	c[1].win = new_boxed_window(conf, y + h - 6, x + w/2 - 2, 3, 4, LOWERED);
161 	mvwaddch(widget, h - 5, w/2 + 2, ':');
162 	c[2].win = new_boxed_window(conf, y + h - 6, x + w/2 + 3, 3, 4, LOWERED);
163 
164 	wrefresh(widget);
165 
166 	sel = 0;
167 	curs_set(2);
168 	loop = true;
169 	while(loop) {
170 		for (i=0; i<3; i++) {
171 			mvwprintw(c[i].win, 1, 1, "%2d", c[i].value);
172 			wrefresh(c[i].win);
173 		}
174 		wmove(c[sel].win, 1, 2);
175 		wrefresh(c[sel].win);
176 
177 		input = getch();
178 		switch(input) {
179 		case KEY_ENTER:
180 		case 10: /* Enter */
181 			output = bs.value[bs.curr];
182 			if (output == BSDDIALOG_OK) {
183 				*hh = c[0].value;
184 				*mm = c[1].value;
185 				*ss = c[2].value;
186 			}
187 			loop = false;
188 			break;
189 		case 27: /* Esc */
190 			output = BSDDIALOG_ESC;
191 			loop = false;
192 			break;
193 		case '\t': /* TAB */
194 			sel = (sel + 1) % 3;
195 			break;
196 		case KEY_LEFT:
197 			if (bs.curr > 0) {
198 				bs.curr--;
199 				draw_buttons(widget, h-2, w, bs, true);
200 				wrefresh(widget);
201 			}
202 			break;
203 		case KEY_RIGHT:
204 			if (bs.curr < (int) bs.nbuttons - 1) {
205 				bs.curr++;
206 				draw_buttons(widget, h-2, w, bs, true);
207 				wrefresh(widget);
208 			}
209 			break;
210 		case KEY_UP:
211 			c[sel].value = c[sel].value < c[sel].max ? c[sel].value + 1 : 0;
212 			break;
213 		case KEY_DOWN:
214 			c[sel].value = c[sel].value > 0 ? c[sel].value - 1 : c[sel].max;
215 			break;
216 		case KEY_F(1):
217 			if (conf->f1_file == NULL && conf->f1_message == NULL)
218 				break;
219 			curs_set(0);
220 			if (f1help(conf) != 0)
221 				return BSDDIALOG_ERROR;
222 			curs_set(2);
223 			/* No break! the terminal size can change */
224 		case KEY_RESIZE:
225 			hide_widget(y, x, h, w,conf->shadow);
226 
227 			/*
228 			 * Unnecessary, but, when the columns decrease the
229 			 * following "refresh" seem not work
230 			 */
231 			refresh();
232 
233 			if (set_widget_size(conf, rows, cols, &h, &w) != 0)
234 				return BSDDIALOG_ERROR;
235 			if (datetime_autosize(conf, rows, cols, &h, &w, MINWTIME, text, bs) != 0)
236 				return BSDDIALOG_ERROR;
237 			if (datetime_checksize(h, w, text, MINWTIME, bs) != 0)
238 				return BSDDIALOG_ERROR;
239 			if (set_widget_position(conf, &y, &x, h, w) != 0)
240 				return BSDDIALOG_ERROR;
241 
242 			wclear(shadow);
243 			mvwin(shadow, y + t.shadow.h, x + t.shadow.w);
244 			wresize(shadow, h, w);
245 
246 			wclear(widget);
247 			mvwin(widget, y, x);
248 			wresize(widget, h, w);
249 
250 			htextpad = 1;
251 			wclear(textpad);
252 			wresize(textpad, 1, w - HBORDERS - t.text.hmargin * 2);
253 
254 			if(update_widget_withtextpad(conf, shadow, widget, h, w,
255 			    RAISED, textpad, &htextpad, text, true) != 0)
256 				return BSDDIALOG_ERROR;
257 
258 			mvwaddch(widget, h - 5, w/2 - 3, ':');
259 			mvwaddch(widget, h - 5, w/2 + 2, ':');
260 
261 			draw_buttons(widget, h-2, w, bs, true);
262 
263 			wrefresh(widget);
264 
265 			prefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
266 
267 			wclear(c[0].win);
268 			mvwin(c[0].win, y + h - 6, x + w/2 - 7);
269 			draw_borders(conf, c[0].win, 3, 4, LOWERED);
270 			wrefresh(c[0].win);
271 
272 			wclear(c[1].win);
273 			mvwin(c[1].win, y + h - 6, x + w/2 - 2);
274 			draw_borders(conf, c[1].win, 3, 4, LOWERED);
275 			wrefresh(c[1].win);
276 
277 			wclear(c[2].win);
278 			mvwin(c[2].win, y + h - 6, x + w/2 + 3);
279 			draw_borders(conf, c[2].win, 3, 4, LOWERED);
280 			wrefresh(c[2].win);
281 
282 			/* Important to avoid grey lines expanding screen */
283 			refresh();
284 			break;
285 		default:
286 			for (i = 0; i < (int) bs.nbuttons; i++)
287 				if (tolower(input) == tolower((bs.label[i])[0])) {
288 					output = bs.value[i];
289 					loop = false;
290 			}
291 		}
292 	}
293 
294 	curs_set(0);
295 
296 	for (i=0; i<3; i++)
297 		delwin(c[i].win);
298 	end_widget_withtextpad(conf, widget, h, w, textpad, shadow);
299 
300 	return output;
301 }
302 
303 int
304 bsddialog_datebox(struct bsddialog_conf *conf, char* text, int rows, int cols,
305     unsigned int *yy, unsigned int *mm, unsigned int *dd)
306 {
307 	WINDOW *widget, *textpad, *shadow;
308 	int i, input, output, y, x, h, w, sel, htextpad;
309 	struct buttons bs;
310 	bool loop;
311 	struct calendar {
312 		int max;
313 		int value;
314 		WINDOW *win;
315 		unsigned int x;
316 	};
317 	struct month {
318 		char *name;
319 		unsigned int days;
320 	};
321 
322 	if (yy == NULL || mm == NULL || dd == NULL)
323 		RETURN_ERROR("yy / mm / dd cannot be NULL");
324 
325 	struct calendar c[3] = {
326 		{9999, *yy, NULL, 4 },
327 		{12,   *mm, NULL, 9 },
328 		{31,   *dd, NULL, 2 }
329 	};
330 
331 	struct month m[12] = {
332 		{ "January", 31 }, { "February", 28 }, { "March",     31 },
333 		{ "April",   30 }, { "May",      31 }, { "June",      30 },
334 		{ "July",    31 }, { "August",   31 }, { "September", 30 },
335 		{ "October", 31 }, { "November", 30 }, { "December",  31 }
336 	};
337 
338 #define ISLEAF(year) ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
339 
340 	for (i = 0 ; i < 3; i++) {
341 		if (c[i].value > c[i].max)
342 			c[i].value = c[i].max;
343 		if (c[i].value < 1)
344 			c[i].value = 1;
345 	}
346 	c[2].max = m[c[1].value -1].days;
347 	if (c[1].value == 2 && ISLEAF(c[0].value))
348 		c[2].max = 29;
349 	if (c[2].value > c[2].max)
350 		c[2].value = c[2].max;
351 
352 	get_buttons(conf, &bs, BUTTONLABEL(ok_label), BUTTONLABEL(extra_label),
353 	    BUTTONLABEL(cancel_label), BUTTONLABEL(help_label));
354 
355 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
356 		return BSDDIALOG_ERROR;
357 	if (datetime_autosize(conf, rows, cols, &h, &w, MINWDATE, text, bs) != 0)
358 		return BSDDIALOG_ERROR;
359 	if (datetime_checksize(h, w, text, MINWDATE, bs) != 0)
360 		return BSDDIALOG_ERROR;
361 	if (set_widget_position(conf, &y, &x, h, w) != 0)
362 		return BSDDIALOG_ERROR;
363 
364 	if (new_widget_withtextpad(conf, &shadow, &widget, y, x, h, w, RAISED,
365 	    &textpad, &htextpad, text, true) != 0)
366 		return BSDDIALOG_ERROR;
367 
368 	draw_buttons(widget, h-2, w, bs, true);
369 
370 	wrefresh(widget);
371 
372 	prefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
373 
374 	c[0].win = new_boxed_window(conf, y + h - 6, x + w/2 - 11, 3, 6, LOWERED);
375 	mvwaddch(widget, h - 5, w/2 - 5, '/');
376 	c[1].win = new_boxed_window(conf, y + h - 6, x + w/2 - 4, 3, 11, LOWERED);
377 	mvwaddch(widget, h - 5, w/2 + 7, '/');
378 	c[2].win = new_boxed_window(conf, y + h - 6, x + w/2 + 8, 3, 4, LOWERED);
379 
380 	wrefresh(widget);
381 
382 	sel = 2;
383 	curs_set(2);
384 	loop = true;
385 	while(loop) {
386 		mvwprintw(c[0].win, 1, 1, "%4d", c[0].value);
387 		mvwprintw(c[1].win, 1, 1, "%9s", m[c[1].value-1].name);
388 		mvwprintw(c[2].win, 1, 1, "%2d", c[2].value);
389 		for (i=0; i<3; i++) {
390 			wrefresh(c[i].win);
391 		}
392 		wmove(c[sel].win, 1, c[sel].x);
393 		wrefresh(c[sel].win);
394 
395 		input = getch();
396 		switch(input) {
397 		case KEY_ENTER:
398 		case 10: /* Enter */
399 			output = bs.value[bs.curr];
400 			if (output == BSDDIALOG_OK) {
401 				*yy = c[0].value;
402 				*mm = c[1].value;
403 				*dd = c[2].value;
404 			}
405 			loop = false;
406 			break;
407 		case 27: /* Esc */
408 			output = BSDDIALOG_ESC;
409 			loop = false;
410 			break;
411 		case '\t': /* TAB */
412 			sel = (sel + 1) % 3;
413 			break;
414 		case KEY_LEFT:
415 			if (bs.curr > 0) {
416 				bs.curr--;
417 				draw_buttons(widget, h-2, w, bs, true);
418 				wrefresh(widget);
419 			}
420 			break;
421 		case KEY_RIGHT:
422 			if (bs.curr < (int) bs.nbuttons - 1) {
423 				bs.curr++;
424 				draw_buttons(widget, h-2, w, bs, true);
425 				wrefresh(widget);
426 			}
427 			break;
428 		case KEY_UP:
429 			c[sel].value = c[sel].value > 1 ? c[sel].value - 1 : c[sel].max ;
430 			/* if mount change */
431 			c[2].max = m[c[1].value -1].days;
432 			/* if year change */
433 			if (c[1].value == 2 && ISLEAF(c[0].value))
434 				c[2].max = 29;
435 			/* set new day */
436 			if (c[2].value > c[2].max)
437 				c[2].value = c[2].max;
438 			break;
439 		case KEY_DOWN:
440 			c[sel].value = c[sel].value < c[sel].max ? c[sel].value + 1 : 1;
441 			/* if mount change */
442 			c[2].max = m[c[1].value -1].days;
443 			/* if year change */
444 			if (c[1].value == 2 && ISLEAF(c[0].value))
445 				c[2].max = 29;
446 			/* set new day */
447 			if (c[2].value > c[2].max)
448 				c[2].value = c[2].max;
449 			break;
450 		case KEY_F(1):
451 			if (conf->f1_file == NULL && conf->f1_message == NULL)
452 				break;
453 			curs_set(0);
454 			if (f1help(conf) != 0)
455 				return BSDDIALOG_ERROR;
456 			curs_set(2);
457 			/* No break! the terminal size can change */
458 		case KEY_RESIZE:
459 			hide_widget(y, x, h, w,conf->shadow);
460 
461 			/*
462 			 * Unnecessary, but, when the columns decrease the
463 			 * following "refresh" seem not work
464 			 */
465 			refresh();
466 
467 			if (set_widget_size(conf, rows, cols, &h, &w) != 0)
468 				return BSDDIALOG_ERROR;
469 			if (datetime_autosize(conf, rows, cols, &h, &w, MINWDATE, text, bs) != 0)
470 				return BSDDIALOG_ERROR;
471 			if (datetime_checksize(h, w, text, MINWDATE, bs) != 0)
472 				return BSDDIALOG_ERROR;
473 			if (set_widget_position(conf, &y, &x, h, w) != 0)
474 				return BSDDIALOG_ERROR;
475 
476 			wclear(shadow);
477 			mvwin(shadow, y + t.shadow.h, x + t.shadow.w);
478 			wresize(shadow, h, w);
479 
480 			wclear(widget);
481 			mvwin(widget, y, x);
482 			wresize(widget, h, w);
483 
484 			htextpad = 1;
485 			wclear(textpad);
486 			wresize(textpad, 1, w - HBORDERS - t.text.hmargin * 2);
487 
488 			if(update_widget_withtextpad(conf, shadow, widget, h, w,
489 			    RAISED, textpad, &htextpad, text, true) != 0)
490 				return BSDDIALOG_ERROR;
491 
492 			mvwaddch(widget, h - 5, w/2 - 5, '/');
493 			mvwaddch(widget, h - 5, w/2 + 7, '/');
494 
495 			draw_buttons(widget, h-2, w, bs, true);
496 
497 			wrefresh(widget);
498 
499 			prefresh(textpad, 0, 0, y+1, x+2, y+h-7, x+w-2);
500 
501 			wclear(c[0].win);
502 			mvwin(c[0].win, y + h - 6, x + w/2 - 11);
503 			draw_borders(conf, c[0].win, 3, 6, LOWERED);
504 			wrefresh(c[0].win);
505 
506 			wclear(c[1].win);
507 			mvwin(c[1].win, y + h - 6, x + w/2 - 4);
508 			draw_borders(conf, c[1].win, 3, 11, LOWERED);
509 			wrefresh(c[1].win);
510 
511 			wclear(c[2].win);
512 			mvwin(c[2].win, y + h - 6, x + w/2 + 8);
513 			draw_borders(conf, c[2].win, 3, 4, LOWERED);
514 			wrefresh(c[2].win);
515 
516 			/* Important to avoid grey lines expanding screen */
517 			refresh();
518 			break;
519 		default:
520 			for (i = 0; i < (int) bs.nbuttons; i++)
521 				if (tolower(input) == tolower((bs.label[i])[0])) {
522 					output = bs.value[i];
523 					loop = false;
524 			}
525 		}
526 	}
527 
528 	curs_set(0);
529 
530 	for (i=0; i<3; i++)
531 		delwin(c[i].win);
532 	end_widget_withtextpad(conf, widget, h, w, textpad, shadow);
533 
534 	return output;
535 }
536