1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2022-2023 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 <curses.h> 29 #include <stdlib.h> 30 #include <string.h> 31 32 #include "bsddialog.h" 33 #include "bsddialog_theme.h" 34 #include "lib_util.h" 35 36 /* Calendar */ 37 #define MIN_YEAR_CAL 0 38 #define MAX_YEAR_CAL 999999999 39 #define MINHCAL 13 40 #define MINWCAL 36 /* 34 calendar, 1 + 1 margins */ 41 /* Datebox */ 42 #define MIN_YEAR_DATE 0 43 #define MAX_YEAR_DATE 9999 44 #define MINWDATE 23 /* 3 windows and their borders */ 45 46 #define ISLEAP(year) ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) 47 48 static int minyear; 49 static int maxyear; 50 51 static const char *m[12] = { 52 "January", 53 "February", 54 "March", 55 "April", 56 "May", 57 "June", 58 "July", 59 "August", 60 "September", 61 "October", 62 "November", 63 "December" 64 }; 65 66 enum operation { 67 UP_DAY, 68 DOWN_DAY, 69 LEFT_DAY, 70 RIGHT_DAY, 71 UP_MONTH, 72 DOWN_MONTH, 73 UP_YEAR, 74 DOWN_YEAR 75 }; 76 77 /* private datebox item */ 78 struct dateitem { 79 enum operation up; 80 enum operation down; 81 WINDOW *win; 82 int width; 83 const char *fmt; 84 int *value; 85 }; 86 87 static int month_days(int yy, int mm) 88 { 89 int days; 90 91 if (mm == 2) 92 days = ISLEAP(yy) ? 29 : 28; 93 else if (mm == 4 || mm == 6 || mm == 9 || mm == 11) 94 days = 30; 95 else 96 days = 31; 97 98 return (days); 99 } 100 101 static int week_day(int yy, int mm, int dd) 102 { 103 int wd; 104 105 dd += mm < 3 ? yy-- : yy - 2; 106 wd = 23*mm/9 + dd + 4 + yy/4 - yy/100 + yy/400; 107 wd %= 7; 108 109 return (wd); 110 } 111 112 static void 113 init_date(unsigned int *year, unsigned int *month, unsigned int *day, int *yy, 114 int *mm, int *dd) 115 { 116 *yy = MIN(*year, (unsigned int)maxyear); 117 if (*yy < minyear) 118 *yy = minyear; 119 *mm = MIN(*month, 12); 120 if (*mm == 0) 121 *mm = 1; 122 *dd = (*day == 0) ? 1 : *day; 123 if(*dd > month_days(*yy, *mm)) 124 *dd = month_days(*yy, *mm); 125 } 126 127 static void datectl(enum operation op, int *yy, int *mm, int *dd) 128 { 129 int ndays; 130 131 ndays = month_days(*yy, *mm); 132 133 switch (op) { 134 case UP_DAY: 135 if (*dd > 7) 136 *dd -= 7; 137 else { 138 if (*mm == 1) { 139 *yy -= 1; 140 *mm = 12; 141 } else 142 *mm -= 1; 143 ndays = month_days(*yy, *mm); 144 *dd = ndays - abs(7 - *dd); 145 } 146 break; 147 case DOWN_DAY: 148 if (*dd + 7 < ndays) 149 *dd += 7; 150 else { 151 if (*mm == 12) { 152 *yy += 1; 153 *mm = 1; 154 } else 155 *mm += 1; 156 *dd = *dd + 7 - ndays; 157 } 158 break; 159 case LEFT_DAY: 160 if (*dd > 1) 161 *dd -= 1; 162 else { 163 if (*mm == 1) { 164 *yy -= 1; 165 *mm = 12; 166 } else 167 *mm -= 1; 168 *dd = month_days(*yy, *mm); 169 } 170 break; 171 case RIGHT_DAY: 172 if (*dd < ndays) 173 *dd += 1; 174 else { 175 if (*mm == 12) { 176 *yy += 1; 177 *mm = 1; 178 } else 179 *mm += 1; 180 *dd = 1; 181 } 182 break; 183 case UP_MONTH: 184 if (*mm == 1) { 185 *mm = 12; 186 *yy -= 1; 187 } else 188 *mm -= 1; 189 ndays = month_days(*yy, *mm); 190 if (*dd > ndays) 191 *dd = ndays; 192 break; 193 case DOWN_MONTH: 194 if (*mm == 12) { 195 *mm = 1; 196 *yy += 1; 197 } else 198 *mm += 1; 199 ndays = month_days(*yy, *mm); 200 if (*dd > ndays) 201 *dd = ndays; 202 break; 203 case UP_YEAR: 204 *yy -= 1; 205 ndays = month_days(*yy, *mm); 206 if (*dd > ndays) 207 *dd = ndays; 208 break; 209 case DOWN_YEAR: 210 *yy += 1; 211 ndays = month_days(*yy, *mm); 212 if (*dd > ndays) 213 *dd = ndays; 214 break; 215 } 216 217 if (*yy < minyear) { 218 *yy = minyear; 219 *mm = 1; 220 *dd = 1; 221 } 222 if (*yy > maxyear) { 223 *yy = maxyear; 224 *mm = 12; 225 *dd = 31; 226 } 227 } 228 229 static void 230 drawsquare(struct bsddialog_conf *conf, WINDOW *win, enum elevation elev, 231 const char *fmt, int value, bool focus) 232 { 233 int h, l, w; 234 235 getmaxyx(win, h, w); 236 draw_borders(conf, win, elev); 237 if (focus) { 238 l = 2 + w%2; 239 wattron(win, t.dialog.arrowcolor); 240 mvwhline(win, 0, w/2 - l/2, 241 conf->ascii_lines ? '^' : ACS_UARROW, l); 242 mvwhline(win, h-1, w/2 - l/2, 243 conf->ascii_lines ? 'v' : ACS_DARROW, l); 244 wattroff(win, t.dialog.arrowcolor); 245 } 246 247 if (focus) 248 wattron(win, t.menu.f_namecolor); 249 if (strchr(fmt, 's') != NULL) 250 mvwprintw(win, 1, 1, fmt, m[value - 1]); 251 else 252 mvwprintw(win, 1, 1, fmt, value); 253 if (focus) 254 wattroff(win, t.menu.f_namecolor); 255 256 wnoutrefresh(win); 257 } 258 259 static void 260 print_calendar(struct bsddialog_conf *conf, WINDOW *win, int yy, int mm, int dd, 261 bool active) 262 { 263 int ndays, i, y, x, wd, h, w; 264 265 getmaxyx(win, h, w); 266 wclear(win); 267 draw_borders(conf, win, RAISED); 268 if (active) { 269 wattron(win, t.dialog.arrowcolor); 270 mvwhline(win, 0, 15, conf->ascii_lines ? '^' : ACS_UARROW, 4); 271 mvwhline(win, h-1, 15, conf->ascii_lines ? 'v' : ACS_DARROW, 4); 272 mvwvline(win, 3, 0, conf->ascii_lines ? '<' : ACS_LARROW, 3); 273 mvwvline(win, 3, w-1, conf->ascii_lines ? '>' : ACS_RARROW, 3); 274 wattroff(win, t.dialog.arrowcolor); 275 } 276 277 mvwaddstr(win, 1, 5, "Sun Mon Tue Wed Thu Fri Sat"); 278 ndays = month_days(yy, mm); 279 y = 2; 280 wd = week_day(yy, mm, 1); 281 for (i = 1; i <= ndays; i++) { 282 x = 5 + (4 * wd); /* x has to be 6 with week number */ 283 wmove(win, y, x); 284 mvwprintw(win, y, x, "%2d", i); 285 if (i == dd) { 286 wattron(win, t.menu.f_namecolor); 287 mvwprintw(win, y, x, "%2d", i); 288 wattroff(win, t.menu.f_namecolor); 289 } 290 wd++; 291 if (wd > 6) { 292 wd = 0; 293 y++; 294 } 295 } 296 297 wnoutrefresh(win); 298 } 299 300 static int 301 calendar_redraw(struct dialog *d, WINDOW *yy_win, WINDOW *mm_win, 302 WINDOW *dd_win) 303 { 304 int ycal, xcal; 305 306 if (d->built) { 307 hide_dialog(d); 308 refresh(); /* Important for decreasing screen */ 309 } 310 if (dialog_size_position(d, MINHCAL, MINWCAL, NULL) != 0) 311 return (BSDDIALOG_ERROR); 312 if (draw_dialog(d) != 0) 313 return (BSDDIALOG_ERROR); 314 if (d->built) 315 refresh(); /* Important to fix grey lines expanding screen */ 316 TEXTPAD(d, MINHCAL + HBUTTONS); 317 318 ycal = d->y + d->h - 15; 319 xcal = d->x + d->w/2 - 17; 320 mvwaddstr(d->widget, d->h - 16, d->w/2 - 17, "Month"); 321 update_box(d->conf, mm_win, ycal, xcal, 3, 17, RAISED); 322 mvwaddstr(d->widget, d->h - 16, d->w/2, "Year"); 323 update_box(d->conf, yy_win, ycal, xcal + 17, 3, 17, RAISED); 324 update_box(d->conf, dd_win, ycal + 3, xcal, 9, 34, RAISED); 325 wnoutrefresh(d->widget); 326 327 return (0); 328 } 329 330 int 331 bsddialog_calendar(struct bsddialog_conf *conf, const char *text, int rows, 332 int cols, unsigned int *year, unsigned int *month, unsigned int *day) 333 { 334 bool loop, focusbuttons; 335 int retval, sel, yy, mm, dd; 336 wint_t input; 337 WINDOW *yy_win, *mm_win, *dd_win; 338 struct dialog d; 339 340 CHECK_PTR(year); 341 CHECK_PTR(month); 342 CHECK_PTR(day); 343 minyear = MIN_YEAR_CAL; 344 maxyear = MAX_YEAR_CAL; 345 init_date(year, month, day, &yy, &mm, &dd); 346 347 if (prepare_dialog(conf, text, rows, cols, &d) != 0) 348 return (BSDDIALOG_ERROR); 349 set_buttons(&d, true, OK_LABEL, CANCEL_LABEL); 350 if ((yy_win = newwin(1, 1, 1, 1)) == NULL) 351 RETURN_ERROR("Cannot build WINDOW for yy"); 352 wbkgd(yy_win, t.dialog.color); 353 if ((mm_win = newwin(1, 1, 1, 1)) == NULL) 354 RETURN_ERROR("Cannot build WINDOW for mm"); 355 wbkgd(mm_win, t.dialog.color); 356 if ((dd_win = newwin(1, 1, 1, 1)) == NULL) 357 RETURN_ERROR("Cannot build WINDOW for dd"); 358 wbkgd(dd_win, t.dialog.color); 359 if (calendar_redraw(&d, yy_win, mm_win, dd_win) != 0) 360 return (BSDDIALOG_ERROR); 361 362 sel = -1; 363 loop = focusbuttons = true; 364 while (loop) { 365 drawsquare(conf, mm_win, RAISED, "%15s", mm, sel == 0); 366 drawsquare(conf, yy_win, RAISED, "%15d", yy, sel == 1); 367 print_calendar(conf, dd_win, yy, mm, dd, sel == 2); 368 doupdate(); 369 370 if (get_wch(&input) == ERR) 371 continue; 372 switch(input) { 373 case KEY_ENTER: 374 case 10: /* Enter */ 375 if (focusbuttons || conf->button.always_active) { 376 retval = BUTTONVALUE(d.bs); 377 loop = false; 378 } 379 break; 380 case 27: /* Esc */ 381 if (conf->key.enable_esc) { 382 retval = BSDDIALOG_ESC; 383 loop = false; 384 } 385 break; 386 case '\t': /* TAB */ 387 if (focusbuttons) { 388 d.bs.curr++; 389 if (d.bs.curr >= (int)d.bs.nbuttons) { 390 focusbuttons = false; 391 sel = 0; 392 d.bs.curr = conf->button.always_active ? 393 0 : -1; 394 } 395 } else { 396 sel++; 397 if (sel > 2) { 398 focusbuttons = true; 399 sel = -1; 400 d.bs.curr = 0; 401 } 402 } 403 DRAW_BUTTONS(d); 404 break; 405 case KEY_RIGHT: 406 if (focusbuttons) { 407 d.bs.curr++; 408 if (d.bs.curr >= (int)d.bs.nbuttons) { 409 focusbuttons = false; 410 sel = 0; 411 d.bs.curr = conf->button.always_active ? 412 0 : -1; 413 } 414 } else if (sel == 2) { 415 datectl(RIGHT_DAY, &yy, &mm, &dd); 416 } else { /* Month or Year*/ 417 sel++; 418 } 419 DRAW_BUTTONS(d); 420 break; 421 case KEY_LEFT: 422 if (focusbuttons) { 423 d.bs.curr--; 424 if (d.bs.curr < 0) { 425 focusbuttons = false; 426 sel = 2; 427 d.bs.curr = conf->button.always_active ? 428 0 : -1; 429 } 430 } else if (sel == 2) { 431 datectl(LEFT_DAY, &yy, &mm, &dd); 432 } else if (sel == 1) { 433 sel = 0; 434 } else { /* sel = 0, Month */ 435 focusbuttons = true; 436 sel = -1; 437 d.bs.curr = 0; 438 } 439 DRAW_BUTTONS(d); 440 break; 441 case KEY_UP: 442 if (focusbuttons) { 443 sel = 2; 444 focusbuttons = false; 445 d.bs.curr = conf->button.always_active ? 0 : -1; 446 DRAW_BUTTONS(d); 447 } else if (sel == 0) { 448 datectl(UP_MONTH, &yy, &mm, &dd); 449 } else if (sel == 1) { 450 datectl(UP_YEAR, &yy, &mm, &dd); 451 } else { /* sel = 2 */ 452 datectl(UP_DAY, &yy, &mm, &dd); 453 } 454 break; 455 case KEY_DOWN: 456 if (focusbuttons) { 457 break; 458 } else if (sel == 0) { 459 datectl(DOWN_MONTH, &yy, &mm, &dd); 460 } else if (sel == 1) { 461 datectl(DOWN_YEAR, &yy, &mm, &dd); 462 } else { /* sel = 2 */ 463 datectl(DOWN_DAY, &yy, &mm, &dd); 464 } 465 break; 466 case KEY_HOME: 467 datectl(UP_MONTH, &yy, &mm, &dd); 468 break; 469 case KEY_END: 470 datectl(DOWN_MONTH, &yy, &mm, &dd); 471 break; 472 case KEY_PPAGE: 473 datectl(UP_YEAR, &yy, &mm, &dd); 474 break; 475 case KEY_NPAGE: 476 datectl(DOWN_YEAR, &yy, &mm, &dd); 477 break; 478 case KEY_F(1): 479 if (conf->key.f1_file == NULL && 480 conf->key.f1_message == NULL) 481 break; 482 if (f1help_dialog(conf) != 0) 483 return (BSDDIALOG_ERROR); 484 if (calendar_redraw(&d, yy_win, mm_win, dd_win) != 0) 485 return (BSDDIALOG_ERROR); 486 break; 487 case KEY_RESIZE: 488 if (calendar_redraw(&d, yy_win, mm_win, dd_win) != 0) 489 return (BSDDIALOG_ERROR); 490 break; 491 default: 492 if (shortcut_buttons(input, &d.bs)) { 493 DRAW_BUTTONS(d); 494 doupdate(); 495 retval = BUTTONVALUE(d.bs); 496 loop = false; 497 } 498 } 499 } 500 501 *year = yy; 502 *month = mm; 503 *day = dd; 504 505 delwin(yy_win); 506 delwin(mm_win); 507 delwin(dd_win); 508 end_dialog(&d); 509 510 return (retval); 511 } 512 513 static int datebox_redraw(struct dialog *d, struct dateitem *di) 514 { 515 int y, x; 516 517 if (d->built) { 518 hide_dialog(d); 519 refresh(); /* Important for decreasing screen */ 520 } 521 if (dialog_size_position(d, 3 /*windows*/, MINWDATE, NULL) != 0) 522 return (BSDDIALOG_ERROR); 523 if (draw_dialog(d) != 0) 524 return (BSDDIALOG_ERROR); 525 if (d->built) 526 refresh(); /* Important to fix grey lines expanding screen */ 527 TEXTPAD(d, 3 /*windows*/ + HBUTTONS); 528 529 y = d->y + d->h - 6; 530 x = (d->x + d->w / 2) - 11; 531 update_box(d->conf, di[0].win, y, x, 3, di[0].width, LOWERED); 532 mvwaddch(d->widget, d->h - 5, x - d->x + di[0].width, '/'); 533 x += di[0].width + 1; 534 update_box(d->conf, di[1].win, y, x , 3, di[1].width, LOWERED); 535 mvwaddch(d->widget, d->h - 5, x - d->x + di[1].width, '/'); 536 x += di[1].width + 1; 537 update_box(d->conf, di[2].win, y, x, 3, di[2].width, LOWERED); 538 wnoutrefresh(d->widget); 539 540 return (0); 541 } 542 543 static int 544 build_dateitem(const char *format, int *yy, int *mm, int *dd, 545 struct dateitem *dt) 546 { 547 int i; 548 wchar_t *wformat; 549 struct dateitem init[3] = { 550 {UP_YEAR, DOWN_YEAR, NULL, 6, "%4d", yy}, 551 {UP_MONTH, DOWN_MONTH, NULL, 11, "%9s", mm}, 552 {LEFT_DAY, RIGHT_DAY, NULL, 4, "%02d", dd}, 553 }; 554 555 for (i = 0; i < 3; i++) { 556 if ((init[i].win = newwin(1, 1, 1, 1)) == NULL) 557 RETURN_FMTERROR("Cannot build WINDOW dateitem[%d]", i); 558 wbkgd(init[i].win, t.dialog.color); 559 } 560 561 if ((wformat = alloc_mbstows(CHECK_STR(format))) == NULL) 562 RETURN_ERROR("Cannot allocate conf.date.format in wchar_t*"); 563 if (format == NULL || wcscmp(wformat, L"d/m/y") == 0) { 564 dt[0] = init[2]; 565 dt[1] = init[1]; 566 dt[2] = init[0]; 567 } else if (wcscmp(wformat, L"m/d/y") == 0) { 568 dt[0] = init[1]; 569 dt[1] = init[2]; 570 dt[2] = init[0]; 571 } else if (wcscmp(wformat, L"y/m/d") == 0) { 572 dt[0] = init[0]; 573 dt[1] = init[1]; 574 dt[2] = init[2]; 575 } else 576 RETURN_FMTERROR("Invalid conf.date.format=\"%s\"", format); 577 free(wformat); 578 579 return (0); 580 } 581 582 int 583 bsddialog_datebox(struct bsddialog_conf *conf, const char *text, int rows, 584 int cols, unsigned int *year, unsigned int *month, unsigned int *day) 585 { 586 bool loop, focusbuttons; 587 int retval, i, sel, yy, mm, dd; 588 wint_t input; 589 struct dateitem di[3]; 590 struct dialog d; 591 592 CHECK_PTR(year); 593 CHECK_PTR(month); 594 CHECK_PTR(day); 595 minyear = MIN_YEAR_DATE; 596 maxyear = MAX_YEAR_DATE; 597 init_date(year, month, day, &yy, &mm, &dd); 598 599 if (prepare_dialog(conf, text, rows, cols, &d) != 0) 600 return (BSDDIALOG_ERROR); 601 set_buttons(&d, true, OK_LABEL, CANCEL_LABEL); 602 if (build_dateitem(conf->date.format, &yy, &mm, &dd, di) != 0) 603 return (BSDDIALOG_ERROR); 604 if (datebox_redraw(&d, di) != 0) 605 return (BSDDIALOG_ERROR); 606 607 sel = -1; 608 loop = focusbuttons = true; 609 while (loop) { 610 for (i = 0; i < 3; i++) 611 drawsquare(conf, di[i].win, LOWERED, di[i].fmt, 612 *di[i].value, sel == i); 613 doupdate(); 614 615 if (get_wch(&input) == ERR) 616 continue; 617 switch(input) { 618 case KEY_ENTER: 619 case 10: /* Enter */ 620 if (focusbuttons || conf->button.always_active) { 621 retval = BUTTONVALUE(d.bs); 622 loop = false; 623 } 624 break; 625 case 27: /* Esc */ 626 if (conf->key.enable_esc) { 627 retval = BSDDIALOG_ESC; 628 loop = false; 629 } 630 break; 631 case KEY_RIGHT: 632 case '\t': /* TAB */ 633 if (focusbuttons) { 634 d.bs.curr++; 635 focusbuttons = d.bs.curr < (int)d.bs.nbuttons ? 636 true : false; 637 if (focusbuttons == false) { 638 sel = 0; 639 d.bs.curr = conf->button.always_active ? 640 0 : -1; 641 } 642 } else { 643 sel++; 644 focusbuttons = sel > 2 ? true : false; 645 if (focusbuttons) { 646 d.bs.curr = 0; 647 } 648 } 649 DRAW_BUTTONS(d); 650 break; 651 case KEY_LEFT: 652 if (focusbuttons) { 653 d.bs.curr--; 654 focusbuttons = d.bs.curr < 0 ? false : true; 655 if (focusbuttons == false) { 656 sel = 2; 657 d.bs.curr = conf->button.always_active ? 658 0 : -1; 659 } 660 } else { 661 sel--; 662 focusbuttons = sel < 0 ? true : false; 663 if (focusbuttons) 664 d.bs.curr = (int)d.bs.nbuttons - 1; 665 } 666 DRAW_BUTTONS(d); 667 break; 668 case KEY_UP: 669 if (focusbuttons) { 670 sel = 0; 671 focusbuttons = false; 672 d.bs.curr = conf->button.always_active ? 0 : -1; 673 DRAW_BUTTONS(d); 674 } else { 675 datectl(di[sel].up, &yy, &mm, &dd); 676 } 677 break; 678 case KEY_DOWN: 679 if (focusbuttons) 680 break; 681 datectl(di[sel].down, &yy, &mm, &dd); 682 break; 683 case KEY_F(1): 684 if (conf->key.f1_file == NULL && 685 conf->key.f1_message == NULL) 686 break; 687 if (f1help_dialog(conf) != 0) 688 return (BSDDIALOG_ERROR); 689 if (datebox_redraw(&d, di) != 0) 690 return (BSDDIALOG_ERROR); 691 break; 692 case KEY_RESIZE: 693 if (datebox_redraw(&d, di) != 0) 694 return (BSDDIALOG_ERROR); 695 break; 696 default: 697 if (shortcut_buttons(input, &d.bs)) { 698 DRAW_BUTTONS(d); 699 doupdate(); 700 retval = BUTTONVALUE(d.bs); 701 loop = false; 702 } 703 } 704 } 705 706 *year = yy; 707 *month = mm; 708 *day = dd; 709 710 for (i = 0; i < 3 ; i++) 711 delwin(di[i].win); 712 end_dialog(&d); 713 714 return (retval); 715 }