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 }