/*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2022-2024 Alfonso Sabato Siciliano * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include #include #include #include "bsddialog.h" #include "bsddialog_theme.h" #include "lib_util.h" /* Calendar */ #define MIN_YEAR_CAL 0 #define MAX_YEAR_CAL 999999999 #define MINHCAL 13 #define MINWCAL 36 /* 34 calendar, 1 + 1 margins */ /* Datebox */ #define MIN_YEAR_DATE 0 #define MAX_YEAR_DATE 9999 #define MINWDATE 23 /* 3 windows and their borders */ #define ISLEAP(year) ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) static int minyear; static int maxyear; static const char *m[12] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; enum operation { UP_DAY, DOWN_DAY, LEFT_DAY, RIGHT_DAY, UP_MONTH, DOWN_MONTH, UP_YEAR, DOWN_YEAR }; /* private datebox item */ struct dateitem { enum operation up; enum operation down; WINDOW *win; int width; const char *fmt; int *value; }; static int month_days(int yy, int mm) { int days; if (mm == 2) days = ISLEAP(yy) ? 29 : 28; else if (mm == 4 || mm == 6 || mm == 9 || mm == 11) days = 30; else days = 31; return (days); } static int week_day(int yy, int mm, int dd) { int wd; dd += mm < 3 ? yy-- : yy - 2; wd = 23*mm/9 + dd + 4 + yy/4 - yy/100 + yy/400; wd %= 7; return (wd); } static void init_date(unsigned int *year, unsigned int *month, unsigned int *day, int *yy, int *mm, int *dd) { *yy = MIN(*year, (unsigned int)maxyear); if (*yy < minyear) *yy = minyear; *mm = MIN(*month, 12); if (*mm == 0) *mm = 1; *dd = (*day == 0) ? 1 : *day; if (*dd > month_days(*yy, *mm)) *dd = month_days(*yy, *mm); } static void datectl(enum operation op, int *yy, int *mm, int *dd) { int ndays; ndays = month_days(*yy, *mm); switch (op) { case UP_DAY: if (*dd > 7) *dd -= 7; else { if (*mm == 1) { *yy -= 1; *mm = 12; } else *mm -= 1; ndays = month_days(*yy, *mm); *dd = ndays - abs(7 - *dd); } break; case DOWN_DAY: if (*dd + 7 < ndays) *dd += 7; else { if (*mm == 12) { *yy += 1; *mm = 1; } else *mm += 1; *dd = *dd + 7 - ndays; } break; case LEFT_DAY: if (*dd > 1) *dd -= 1; else { if (*mm == 1) { *yy -= 1; *mm = 12; } else *mm -= 1; *dd = month_days(*yy, *mm); } break; case RIGHT_DAY: if (*dd < ndays) *dd += 1; else { if (*mm == 12) { *yy += 1; *mm = 1; } else *mm += 1; *dd = 1; } break; case UP_MONTH: if (*mm == 1) { *mm = 12; *yy -= 1; } else *mm -= 1; ndays = month_days(*yy, *mm); if (*dd > ndays) *dd = ndays; break; case DOWN_MONTH: if (*mm == 12) { *mm = 1; *yy += 1; } else *mm += 1; ndays = month_days(*yy, *mm); if (*dd > ndays) *dd = ndays; break; case UP_YEAR: *yy -= 1; ndays = month_days(*yy, *mm); if (*dd > ndays) *dd = ndays; break; case DOWN_YEAR: *yy += 1; ndays = month_days(*yy, *mm); if (*dd > ndays) *dd = ndays; break; } if (*yy < minyear) { *yy = minyear; *mm = 1; *dd = 1; } if (*yy > maxyear) { *yy = maxyear; *mm = 12; *dd = 31; } } static void drawsquare(struct bsddialog_conf *conf, WINDOW *win, enum elevation elev, const char *fmt, int value, bool focus) { int h, l, w; getmaxyx(win, h, w); draw_borders(conf, win, elev); if (focus) { l = 2 + w%2; wattron(win, t.dialog.arrowcolor); mvwhline(win, 0, w/2 - l/2, UARROW(conf), l); mvwhline(win, h-1, w/2 - l/2, DARROW(conf), l); wattroff(win, t.dialog.arrowcolor); } if (focus) wattron(win, t.menu.f_namecolor); if (strchr(fmt, 's') != NULL) mvwprintw(win, 1, 1, fmt, m[value - 1]); else mvwprintw(win, 1, 1, fmt, value); if (focus) wattroff(win, t.menu.f_namecolor); wnoutrefresh(win); } static void print_calendar(struct bsddialog_conf *conf, WINDOW *win, int yy, int mm, int dd, bool active) { int ndays, i, y, x, wd, h, w; getmaxyx(win, h, w); wclear(win); draw_borders(conf, win, RAISED); if (active) { wattron(win, t.dialog.arrowcolor); mvwhline(win, 0, 15, UARROW(conf), 4); mvwhline(win, h-1, 15, DARROW(conf), 4); mvwvline(win, 3, 0, LARROW(conf), 3); mvwvline(win, 3, w-1, RARROW(conf), 3); wattroff(win, t.dialog.arrowcolor); } mvwaddstr(win, 1, 5, "Sun Mon Tue Wed Thu Fri Sat"); ndays = month_days(yy, mm); y = 2; wd = week_day(yy, mm, 1); for (i = 1; i <= ndays; i++) { x = 5 + (4 * wd); /* x has to be 6 with week number */ wmove(win, y, x); mvwprintw(win, y, x, "%2d", i); if (i == dd) { wattron(win, t.menu.f_namecolor); mvwprintw(win, y, x, "%2d", i); wattroff(win, t.menu.f_namecolor); } wd++; if (wd > 6) { wd = 0; y++; } } wnoutrefresh(win); } static int calendar_redraw(struct dialog *d, WINDOW *yy_win, WINDOW *mm_win, WINDOW *dd_win) { int ycal, xcal; if (d->built) { hide_dialog(d); refresh(); /* Important for decreasing screen */ } if (dialog_size_position(d, MINHCAL, MINWCAL, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) return (BSDDIALOG_ERROR); if (d->built) refresh(); /* Important to fix grey lines expanding screen */ TEXTPAD(d, MINHCAL + HBUTTONS); ycal = d->y + d->h - 15; xcal = d->x + d->w/2 - 17; mvwaddstr(d->widget, d->h - 16, d->w/2 - 17, "Month"); update_box(d->conf, mm_win, ycal, xcal, 3, 17, RAISED); mvwaddstr(d->widget, d->h - 16, d->w/2, "Year"); update_box(d->conf, yy_win, ycal, xcal + 17, 3, 17, RAISED); update_box(d->conf, dd_win, ycal + 3, xcal, 9, 34, RAISED); wnoutrefresh(d->widget); return (0); } int bsddialog_calendar(struct bsddialog_conf *conf, const char *text, int rows, int cols, unsigned int *year, unsigned int *month, unsigned int *day) { bool loop, focusbuttons; int retval, sel, yy, mm, dd; wint_t input; WINDOW *yy_win, *mm_win, *dd_win; struct dialog d; CHECK_PTR(year); CHECK_PTR(month); CHECK_PTR(day); minyear = MIN_YEAR_CAL; maxyear = MAX_YEAR_CAL; init_date(year, month, day, &yy, &mm, &dd); if (prepare_dialog(conf, text, rows, cols, &d) != 0) return (BSDDIALOG_ERROR); set_buttons(&d, true, OK_LABEL, CANCEL_LABEL); if ((yy_win = newwin(1, 1, 1, 1)) == NULL) RETURN_ERROR("Cannot build WINDOW for yy"); wbkgd(yy_win, t.dialog.color); if ((mm_win = newwin(1, 1, 1, 1)) == NULL) RETURN_ERROR("Cannot build WINDOW for mm"); wbkgd(mm_win, t.dialog.color); if ((dd_win = newwin(1, 1, 1, 1)) == NULL) RETURN_ERROR("Cannot build WINDOW for dd"); wbkgd(dd_win, t.dialog.color); if (calendar_redraw(&d, yy_win, mm_win, dd_win) != 0) return (BSDDIALOG_ERROR); sel = -1; loop = focusbuttons = true; while (loop) { drawsquare(conf, mm_win, RAISED, "%15s", mm, sel == 0); drawsquare(conf, yy_win, RAISED, "%15d", yy, sel == 1); print_calendar(conf, dd_win, yy, mm, dd, sel == 2); doupdate(); if (get_wch(&input) == ERR) continue; switch(input) { case KEY_ENTER: case 10: /* Enter */ if (focusbuttons || conf->button.always_active) { retval = BUTTONVALUE(d.bs); loop = false; } break; case 27: /* Esc */ if (conf->key.enable_esc) { retval = BSDDIALOG_ESC; loop = false; } break; case '\t': /* TAB */ if (focusbuttons) { d.bs.curr++; if (d.bs.curr >= (int)d.bs.nbuttons) { focusbuttons = false; sel = 0; d.bs.curr = conf->button.always_active ? 0 : -1; } } else { sel++; if (sel > 2) { focusbuttons = true; sel = -1; d.bs.curr = 0; } } DRAW_BUTTONS(d); break; case KEY_CTRL('n'): case KEY_RIGHT: if (focusbuttons) { d.bs.curr++; if (d.bs.curr >= (int)d.bs.nbuttons) { focusbuttons = false; sel = 0; d.bs.curr = conf->button.always_active ? 0 : -1; } } else if (sel == 2) { datectl(RIGHT_DAY, &yy, &mm, &dd); } else { /* Month or Year*/ sel++; } DRAW_BUTTONS(d); break; case KEY_CTRL('p'): case KEY_LEFT: if (focusbuttons) { d.bs.curr--; if (d.bs.curr < 0) { focusbuttons = false; sel = 2; d.bs.curr = conf->button.always_active ? 0 : -1; } } else if (sel == 2) { datectl(LEFT_DAY, &yy, &mm, &dd); } else if (sel == 1) { sel = 0; } else { /* sel = 0, Month */ focusbuttons = true; sel = -1; d.bs.curr = 0; } DRAW_BUTTONS(d); break; case KEY_UP: if (focusbuttons) { sel = 2; focusbuttons = false; d.bs.curr = conf->button.always_active ? 0 : -1; DRAW_BUTTONS(d); } else if (sel == 0) { datectl(UP_MONTH, &yy, &mm, &dd); } else if (sel == 1) { datectl(UP_YEAR, &yy, &mm, &dd); } else { /* sel = 2 */ datectl(UP_DAY, &yy, &mm, &dd); } break; case KEY_DOWN: if (focusbuttons) { break; } else if (sel == 0) { datectl(DOWN_MONTH, &yy, &mm, &dd); } else if (sel == 1) { datectl(DOWN_YEAR, &yy, &mm, &dd); } else { /* sel = 2 */ datectl(DOWN_DAY, &yy, &mm, &dd); } break; case '-': if (focusbuttons) { break; } else if (sel == 0) { datectl(UP_MONTH, &yy, &mm, &dd); } else if (sel == 1) { datectl(UP_YEAR, &yy, &mm, &dd); } else { /* sel = 2 */ datectl(LEFT_DAY, &yy, &mm, &dd); } break; case '+': if (focusbuttons) { break; } else if (sel == 0) { datectl(DOWN_MONTH, &yy, &mm, &dd); } else if (sel == 1) { datectl(DOWN_YEAR, &yy, &mm, &dd); } else { /* sel = 2 */ datectl(RIGHT_DAY, &yy, &mm, &dd); } break; case KEY_HOME: datectl(UP_MONTH, &yy, &mm, &dd); break; case KEY_END: datectl(DOWN_MONTH, &yy, &mm, &dd); break; case KEY_PPAGE: datectl(UP_YEAR, &yy, &mm, &dd); break; case KEY_NPAGE: datectl(DOWN_YEAR, &yy, &mm, &dd); break; case KEY_F(1): if (conf->key.f1_file == NULL && conf->key.f1_message == NULL) break; if (f1help_dialog(conf) != 0) return (BSDDIALOG_ERROR); if (calendar_redraw(&d, yy_win, mm_win, dd_win) != 0) return (BSDDIALOG_ERROR); break; case KEY_CTRL('l'): case KEY_RESIZE: if (calendar_redraw(&d, yy_win, mm_win, dd_win) != 0) return (BSDDIALOG_ERROR); break; default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); doupdate(); retval = BUTTONVALUE(d.bs); loop = false; } } } *year = yy; *month = mm; *day = dd; delwin(yy_win); delwin(mm_win); delwin(dd_win); end_dialog(&d); return (retval); } static int datebox_redraw(struct dialog *d, struct dateitem *di) { int y, x; if (d->built) { hide_dialog(d); refresh(); /* Important for decreasing screen */ } if (dialog_size_position(d, 3 /*windows*/, MINWDATE, NULL) != 0) return (BSDDIALOG_ERROR); if (draw_dialog(d) != 0) return (BSDDIALOG_ERROR); if (d->built) refresh(); /* Important to fix grey lines expanding screen */ TEXTPAD(d, 3 /*windows*/ + HBUTTONS); y = d->y + d->h - 6; x = (d->x + d->w / 2) - 11; update_box(d->conf, di[0].win, y, x, 3, di[0].width, LOWERED); mvwaddch(d->widget, d->h - 5, x - d->x + di[0].width, '/'); x += di[0].width + 1; update_box(d->conf, di[1].win, y, x , 3, di[1].width, LOWERED); mvwaddch(d->widget, d->h - 5, x - d->x + di[1].width, '/'); x += di[1].width + 1; update_box(d->conf, di[2].win, y, x, 3, di[2].width, LOWERED); wnoutrefresh(d->widget); return (0); } static int build_dateitem(const char *format, int *yy, int *mm, int *dd, struct dateitem *dt) { int i; wchar_t *wformat; struct dateitem init[3] = { {UP_YEAR, DOWN_YEAR, NULL, 6, "%4d", yy}, {UP_MONTH, DOWN_MONTH, NULL, 11, "%9s", mm}, {LEFT_DAY, RIGHT_DAY, NULL, 4, "%02d", dd}, }; for (i = 0; i < 3; i++) { if ((init[i].win = newwin(1, 1, 1, 1)) == NULL) RETURN_FMTERROR("Cannot build WINDOW dateitem[%d]", i); wbkgd(init[i].win, t.dialog.color); } if ((wformat = alloc_mbstows(CHECK_STR(format))) == NULL) RETURN_ERROR("Cannot allocate conf.date.format in wchar_t*"); if (format == NULL || wcscmp(wformat, L"d/m/y") == 0) { dt[0] = init[2]; dt[1] = init[1]; dt[2] = init[0]; } else if (wcscmp(wformat, L"m/d/y") == 0) { dt[0] = init[1]; dt[1] = init[2]; dt[2] = init[0]; } else if (wcscmp(wformat, L"y/m/d") == 0) { dt[0] = init[0]; dt[1] = init[1]; dt[2] = init[2]; } else RETURN_FMTERROR("Invalid conf.date.format=\"%s\"", format); free(wformat); return (0); } int bsddialog_datebox(struct bsddialog_conf *conf, const char *text, int rows, int cols, unsigned int *year, unsigned int *month, unsigned int *day) { bool loop, focusbuttons; int retval, i, sel, yy, mm, dd; wint_t input; struct dateitem di[3]; struct dialog d; CHECK_PTR(year); CHECK_PTR(month); CHECK_PTR(day); minyear = MIN_YEAR_DATE; maxyear = MAX_YEAR_DATE; init_date(year, month, day, &yy, &mm, &dd); if (prepare_dialog(conf, text, rows, cols, &d) != 0) return (BSDDIALOG_ERROR); set_buttons(&d, true, OK_LABEL, CANCEL_LABEL); if (build_dateitem(conf->date.format, &yy, &mm, &dd, di) != 0) return (BSDDIALOG_ERROR); if (datebox_redraw(&d, di) != 0) return (BSDDIALOG_ERROR); sel = -1; loop = focusbuttons = true; while (loop) { for (i = 0; i < 3; i++) drawsquare(conf, di[i].win, LOWERED, di[i].fmt, *di[i].value, sel == i); doupdate(); if (get_wch(&input) == ERR) continue; switch(input) { case KEY_ENTER: case 10: /* Enter */ if (focusbuttons || conf->button.always_active) { retval = BUTTONVALUE(d.bs); loop = false; } break; case 27: /* Esc */ if (conf->key.enable_esc) { retval = BSDDIALOG_ESC; loop = false; } break; case '\t': /* TAB */ case KEY_CTRL('n'): case KEY_RIGHT: if (focusbuttons) { d.bs.curr++; focusbuttons = d.bs.curr < (int)d.bs.nbuttons ? true : false; if (focusbuttons == false) { sel = 0; d.bs.curr = conf->button.always_active ? 0 : -1; } } else { sel++; focusbuttons = sel > 2 ? true : false; if (focusbuttons) { d.bs.curr = 0; } } DRAW_BUTTONS(d); break; case KEY_CTRL('p'): case KEY_LEFT: if (focusbuttons) { d.bs.curr--; focusbuttons = d.bs.curr < 0 ? false : true; if (focusbuttons == false) { sel = 2; d.bs.curr = conf->button.always_active ? 0 : -1; } } else { sel--; focusbuttons = sel < 0 ? true : false; if (focusbuttons) d.bs.curr = (int)d.bs.nbuttons - 1; } DRAW_BUTTONS(d); break; case '-': if (focusbuttons == false) datectl(di[sel].up, &yy, &mm, &dd); break; case KEY_UP: if (focusbuttons) { sel = 0; focusbuttons = false; d.bs.curr = conf->button.always_active ? 0 : -1; DRAW_BUTTONS(d); } else { datectl(di[sel].up, &yy, &mm, &dd); } break; case '+': case KEY_DOWN: if (focusbuttons) break; datectl(di[sel].down, &yy, &mm, &dd); break; case KEY_F(1): if (conf->key.f1_file == NULL && conf->key.f1_message == NULL) break; if (f1help_dialog(conf) != 0) return (BSDDIALOG_ERROR); if (datebox_redraw(&d, di) != 0) return (BSDDIALOG_ERROR); break; case KEY_CTRL('l'): case KEY_RESIZE: if (datebox_redraw(&d, di) != 0) return (BSDDIALOG_ERROR); break; default: if (shortcut_buttons(input, &d.bs)) { DRAW_BUTTONS(d); doupdate(); retval = BUTTONVALUE(d.bs); loop = false; } } } *year = yy; *month = mm; *day = dd; for (i = 0; i < 3 ; i++) delwin(di[i].win); end_dialog(&d); return (retval); }