1 /* 2 * Copyright 2004-2010, Axel Dörfler, axeld@pinc-software.de. 3 * Copyright 2011, Rene Gollent, rene@gollent.com. 4 * Distributed under the terms of the MIT License. 5 */ 6 7 8 #include <boot/platform.h> 9 #include <boot/menu.h> 10 #include <boot/platform/generic/text_console.h> 11 #include <boot/platform/generic/text_menu.h> 12 13 #include <string.h> 14 15 16 // position 17 static const int32 kFirstLine = 8; 18 static const int32 kOffsetX = 10; 19 static const int32 kHelpLines = 3; 20 21 // colors 22 static const console_color kBackgroundColor = BLACK; 23 static const console_color kTextColor = WHITE; 24 static const console_color kCopyrightColor = CYAN; 25 static const console_color kTitleColor = YELLOW; 26 static const console_color kTitleBackgroundColor = kBackgroundColor; 27 static const console_color kHelpTextColor = WHITE; 28 29 static const console_color kItemColor = GRAY; 30 static const console_color kSelectedItemColor = WHITE; 31 static const console_color kItemBackgroundColor = kBackgroundColor; 32 static const console_color kSelectedItemBackgroundColor = GRAY; 33 static const console_color kDisabledColor = DARK_GRAY; 34 35 static const console_color kSliderColor = CYAN; 36 static const console_color kSliderBackgroundColor = DARK_GRAY; 37 static const console_color kArrowColor = GRAY; 38 39 static int32 sMenuOffset = 0; 40 41 42 static void run_menu(Menu* menu); 43 44 45 static int32 46 menu_height() 47 { 48 return console_height() - kFirstLine - 1 - kHelpLines; 49 } 50 51 52 static void 53 print_spacing(int32 count) 54 { 55 for (int32 i = 0; i < count; i++) 56 putchar(' '); 57 } 58 59 60 static void 61 print_centered(int32 line, const char *text, bool resetPosition = true) 62 { 63 console_set_cursor(console_width() / 2 - strlen(text) / 2, line); 64 printf("%s", text); 65 66 if (resetPosition) { 67 console_set_cursor(0, 0); 68 // this avoids unwanted line feeds 69 } 70 } 71 72 73 static void 74 print_item_at(int32 line, MenuItem *item, bool clearHelp = true) 75 { 76 bool selected = item->IsSelected(); 77 78 line -= sMenuOffset; 79 if (line < 0 || line >= menu_height()) 80 return; 81 82 console_color background = selected 83 ? kSelectedItemBackgroundColor : kItemBackgroundColor; 84 console_color foreground = selected 85 ? kSelectedItemColor : kItemColor; 86 87 if (!item->IsEnabled()) 88 foreground = kDisabledColor; 89 90 console_set_cursor(kOffsetX, line + kFirstLine); 91 console_set_color(foreground, background); 92 93 size_t length = strlen(item->Label()) + 1; 94 95 if (item->Type() == MENU_ITEM_MARKABLE) { 96 console_set_color(DARK_GRAY, background); 97 printf(" ["); 98 console_set_color(foreground, background); 99 printf("%c", item->IsMarked() ? 'x' : ' '); 100 console_set_color(DARK_GRAY, background); 101 printf("] "); 102 console_set_color(foreground, background); 103 104 length += 4; 105 } else 106 printf(" "); 107 108 printf(item->Label()); 109 110 if (item->Submenu() && item->Submenu()->Type() == CHOICE_MENU) { 111 // show the current choice (if any) 112 const char *text = " (Current: "; 113 printf(text); 114 length += strlen(text); 115 116 Menu *subMenu = item->Submenu(); 117 if (subMenu->ChoiceText() != NULL) 118 text = subMenu->ChoiceText(); 119 else 120 text = "None"; 121 length += strlen(text); 122 123 console_set_color(selected ? DARK_GRAY : WHITE, background); 124 125 printf(text); 126 127 console_set_color(foreground, background); 128 putchar(')'); 129 length++; 130 } 131 132 print_spacing(console_width() - length - 2*kOffsetX); 133 134 if (!selected) 135 return; 136 137 console_set_cursor(0, console_height() - kHelpLines); 138 console_set_color(kHelpTextColor, kBackgroundColor); 139 140 if (clearHelp) { 141 // clear help text area 142 for (int32 i = 0; i < console_width() - 1; i++) 143 putchar(' '); 144 putchar('\n'); 145 for (int32 i = 0; i < console_width() - 1; i++) 146 putchar(' '); 147 148 console_set_cursor(0, console_height() - kHelpLines); 149 } 150 151 if (item->HelpText() != NULL) { 152 // show help text at the bottom of the screen, 153 // center it, and wrap it correctly 154 155 const char *text = item->HelpText(); 156 int32 width = console_width() - 2 * kOffsetX; 157 int32 length = strlen(text); 158 159 if (length > width * 2) 160 width += 2 * kOffsetX - 1; 161 162 char* buffer = (char*)malloc(width + 1); 163 if (buffer == NULL) 164 return; 165 buffer[width] = '\0'; 166 // make sure the buffer is always terminated 167 168 int32 row = 0; 169 170 for (int32 i = 0; i < length && row < 2; i++) { 171 while (text[i] == ' ') 172 i++; 173 174 // copy as much bytes as possible 175 int32 bytes = width; 176 if (bytes > length - i) 177 bytes = length - i; 178 179 memcpy(buffer, text + i, bytes); 180 buffer[bytes] = '\0'; 181 182 char *pos = strchr(buffer, '\n'); 183 if (pos != NULL) 184 bytes = pos - buffer; 185 else if (bytes < length - i) { 186 // search for possible line breaks 187 pos = strrchr(buffer, ' '); 188 if (pos != NULL) 189 bytes = pos - buffer; 190 else { 191 // no wrapping possible 192 } 193 } 194 195 i += bytes; 196 buffer[bytes] = '\0'; 197 print_centered(console_height() - kHelpLines + row, buffer); 198 row++; 199 } 200 201 free(buffer); 202 } 203 } 204 205 206 static void 207 draw_menu(Menu *menu) 208 { 209 console_set_color(kTextColor, kBackgroundColor); 210 console_clear_screen(); 211 212 print_centered(1, "Welcome to the"); 213 print_centered(2, "Haiku Boot Loader"); 214 215 console_set_color(kCopyrightColor, kBackgroundColor); 216 print_centered(4, "Copyright 2004-2020 Haiku, Inc."); 217 218 if (menu->Title()) { 219 console_set_cursor(kOffsetX, kFirstLine - 2); 220 console_set_color(kTitleColor, kTitleBackgroundColor); 221 222 printf(" %s", menu->Title()); 223 print_spacing(console_width() - 1 224 - strlen(menu->Title()) - 2 * kOffsetX); 225 } 226 227 MenuItemIterator iterator = menu->ItemIterator(); 228 MenuItem *item; 229 int32 i = 0; 230 231 while ((item = iterator.Next()) != NULL) { 232 if (item->Type() == MENU_ITEM_SEPARATOR) { 233 putchar('\n'); 234 i++; 235 continue; 236 } 237 238 print_item_at(i++, item, false); 239 } 240 241 int32 height = menu_height(); 242 if (menu->CountItems() >= height) { 243 int32 x = console_width() - kOffsetX; 244 console_set_cursor(x, kFirstLine); 245 console_set_color(kArrowColor, kBackgroundColor); 246 putchar(30/*24*/); 247 height--; 248 249 int32 start = sMenuOffset * height / menu->CountItems(); 250 int32 end = (sMenuOffset + height) * height / menu->CountItems(); 251 252 for (i = 1; i < height; i++) { 253 console_set_cursor(x, kFirstLine + i); 254 if (i >= start && i <= end) 255 console_set_color(WHITE, kSliderColor); 256 else 257 console_set_color(WHITE, kSliderBackgroundColor); 258 259 putchar(' '); 260 } 261 262 console_set_cursor(x, kFirstLine + i); 263 console_set_color(kArrowColor, kBackgroundColor); 264 putchar(31/*25*/); 265 } 266 } 267 268 269 static int32 270 first_selectable_item(Menu *menu) 271 { 272 int32 index = -1; 273 MenuItem *item; 274 275 while ((item = menu->ItemAt(++index)) != NULL) { 276 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 277 break; 278 } 279 280 return index; 281 } 282 283 284 static int32 285 last_selectable_item(Menu *menu) 286 { 287 int32 index = menu->CountItems(); 288 MenuItem *item; 289 290 while ((item = menu->ItemAt(--index)) != NULL) { 291 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 292 break; 293 } 294 295 return index; 296 } 297 298 299 static bool 300 make_item_visible(Menu *menu, int32 selected) 301 { 302 if (sMenuOffset > selected 303 || sMenuOffset + menu_height() <= selected) { 304 if (sMenuOffset > selected) 305 sMenuOffset = selected; 306 else 307 sMenuOffset = selected + 1 - menu_height(); 308 309 draw_menu(menu); 310 return true; 311 } 312 313 return false; 314 } 315 316 317 static int32 318 select_previous_valid_item(Menu *menu, int32 selected) 319 { 320 MenuItem *item; 321 while ((item = menu->ItemAt(selected)) != NULL) { 322 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 323 break; 324 325 selected--; 326 } 327 328 if (selected < 0) 329 return first_selectable_item(menu); 330 331 return selected; 332 } 333 334 335 static int32 336 select_next_valid_item(Menu *menu, int32 selected) 337 { 338 MenuItem *item; 339 while ((item = menu->ItemAt(selected)) != NULL) { 340 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 341 break; 342 343 selected++; 344 } 345 346 if (selected >= menu->CountItems()) 347 return last_selectable_item(menu); 348 349 return selected; 350 } 351 352 353 static bool 354 invoke_item(Menu* menu, MenuItem* item, int32& selected, char key) 355 { 356 // leave the menu 357 if (item->Submenu() != NULL && key == TEXT_CONSOLE_KEY_RETURN) { 358 int32 offset = sMenuOffset; 359 menu->Hide(); 360 361 run_menu(item->Submenu()); 362 if (item->Target() != NULL) 363 (*item->Target())(menu, item); 364 365 // restore current menu 366 sMenuOffset = offset; 367 menu->FindSelected(&selected); 368 menu->Show(); 369 draw_menu(menu); 370 } else if (item->Type() == MENU_ITEM_MARKABLE) { 371 // toggle state 372 item->SetMarked(!item->IsMarked()); 373 print_item_at(selected, item); 374 375 if (item->Target() != NULL) 376 (*item->Target())(menu, item); 377 } else if (key == TEXT_CONSOLE_KEY_RETURN) { 378 // the space key does not exit the menu, only return does 379 if (menu->Type() == CHOICE_MENU 380 && item->Type() != MENU_ITEM_NO_CHOICE 381 && item->Type() != MENU_ITEM_TITLE) 382 item->SetMarked(true); 383 384 if (item->Target() != NULL) 385 (*item->Target())(menu, item); 386 return true; 387 } 388 389 return false; 390 } 391 392 393 static void 394 run_menu(Menu* menu) 395 { 396 sMenuOffset = 0; 397 menu->Entered(); 398 menu->Show(); 399 400 draw_menu(menu); 401 402 // Get selected entry, or select the last one, if there is none 403 int32 selected; 404 MenuItem *item = menu->FindSelected(&selected); 405 if (item == NULL) { 406 selected = 0; 407 item = menu->ItemAt(selected); 408 if (item != NULL) 409 item->Select(true); 410 } 411 412 make_item_visible(menu, selected); 413 414 while (true) { 415 int key = console_wait_for_key(); 416 417 item = menu->ItemAt(selected); 418 419 if (TEXT_CONSOLE_IS_CURSOR_KEY(key) || key == 'j' || key == 'J' 420 || key == 'k' || key == 'K') { 421 if (item == NULL) 422 continue; 423 424 int32 oldSelected = selected; 425 426 switch (key) { 427 case TEXT_CONSOLE_KEY_UP: 428 case 'k': 429 case 'K': 430 selected = select_previous_valid_item(menu, selected - 1); 431 break; 432 case TEXT_CONSOLE_KEY_DOWN: 433 case 'j': 434 case 'J': 435 selected = select_next_valid_item(menu, selected + 1); 436 break; 437 case TEXT_CONSOLE_KEY_PAGE_UP: 438 case TEXT_CONSOLE_KEY_LEFT: 439 selected = select_previous_valid_item(menu, 440 selected - menu_height() + 1); 441 break; 442 case TEXT_CONSOLE_KEY_PAGE_DOWN: 443 case TEXT_CONSOLE_KEY_RIGHT: 444 selected = select_next_valid_item(menu, 445 selected + menu_height() - 1); 446 break; 447 case TEXT_CONSOLE_KEY_HOME: 448 selected = first_selectable_item(menu); 449 break; 450 case TEXT_CONSOLE_KEY_END: 451 selected = last_selectable_item(menu); 452 break; 453 } 454 455 // check if selected has changed 456 if (selected != oldSelected) { 457 MenuItem *item = menu->ItemAt(selected); 458 if (item != NULL) 459 item->Select(true); 460 461 make_item_visible(menu, selected); 462 // make sure that the new selected entry is visible 463 if (sMenuOffset > selected 464 || sMenuOffset + menu_height() <= selected) { 465 if (sMenuOffset > selected) 466 sMenuOffset = selected; 467 else 468 sMenuOffset = selected + 1 - menu_height(); 469 470 draw_menu(menu); 471 } 472 } 473 } else if (key == TEXT_CONSOLE_KEY_RETURN 474 || key == TEXT_CONSOLE_KEY_SPACE) { 475 if (item != NULL && invoke_item(menu, item, selected, key)) 476 break; 477 } else if (key == '\t') { 478 if (item == NULL) 479 continue; 480 481 int32 oldSelected = selected; 482 483 // Use tab to cycle between items (on some platforms, arrow keys 484 // are not available) 485 selected = select_next_valid_item(menu, selected + 1); 486 487 if (selected == oldSelected) 488 selected = first_selectable_item(menu); 489 490 // check if selected has changed 491 if (selected != oldSelected) { 492 MenuItem *item = menu->ItemAt(selected); 493 if (item != NULL) 494 item->Select(true); 495 496 make_item_visible(menu, selected); 497 // make sure that the new selected entry is visible 498 if (sMenuOffset > selected 499 || sMenuOffset + menu_height() <= selected) { 500 if (sMenuOffset > selected) 501 sMenuOffset = selected; 502 else 503 sMenuOffset = selected + 1 - menu_height(); 504 505 draw_menu(menu); 506 } 507 } 508 } else if (key == TEXT_CONSOLE_KEY_ESCAPE 509 && menu->Type() != MAIN_MENU) { 510 // escape key was hit 511 break; 512 } else { 513 // Shortcut processing 514 shortcut_hook function = menu->FindShortcut(key); 515 if (function != NULL) 516 function(key); 517 else { 518 item = menu->FindItemByShortcut(key); 519 if (item != NULL && invoke_item(menu, item, selected, 520 TEXT_CONSOLE_KEY_RETURN)) { 521 break; 522 } 523 } 524 } 525 } 526 527 menu->Hide(); 528 menu->Exited(); 529 } 530 531 532 // #pragma mark - 533 534 535 void 536 platform_generic_update_text_menu_item(Menu *menu, MenuItem *item) 537 { 538 if (menu->IsHidden()) 539 return; 540 541 int32 index = menu->IndexOf(item); 542 if (index == -1) 543 return; 544 545 print_item_at(index, item); 546 } 547 548 549 void 550 platform_generic_run_text_menu(Menu *menu) 551 { 552 // platform_switch_to_text_mode(); 553 554 run_menu(menu); 555 556 // platform_switch_to_logo(); 557 } 558 559 560 size_t 561 platform_generic_get_user_input_text(Menu* menu, MenuItem* item, char* buffer, 562 size_t bufferSize) 563 { 564 size_t pos = 0; 565 566 memset(buffer, 0, bufferSize); 567 568 int32 promptLength = strlen(item->Label()) + 2; 569 int32 line = menu->IndexOf(item) - sMenuOffset; 570 if (line < 0 || line >= menu_height()) 571 return 0; 572 573 line += kFirstLine; 574 console_set_cursor(kOffsetX, line); 575 int32 x = kOffsetX + 1; 576 console_set_cursor(0, line); 577 console_set_color(kSelectedItemColor, kSelectedItemBackgroundColor); 578 print_spacing(console_width()); 579 console_set_color(kTextColor, kBackgroundColor); 580 console_set_cursor(0, line); 581 print_spacing(x); 582 printf(item->Label()); 583 printf(": "); 584 x += promptLength; 585 console_set_color(kSelectedItemColor, kSelectedItemBackgroundColor); 586 console_show_cursor(); 587 console_set_cursor(x, line); 588 589 int32 scrollOffset = 0; 590 bool doScroll = false; 591 int key = 0; 592 size_t dataLength = 0; 593 while (true) { 594 key = console_wait_for_key(); 595 if (key == TEXT_CONSOLE_KEY_RETURN || key == TEXT_CONSOLE_KEY_ESCAPE) 596 break; 597 else if (key >= TEXT_CONSOLE_CURSOR_KEYS_START 598 && key < TEXT_CONSOLE_CURSOR_KEYS_END) 599 { 600 switch (key) { 601 case TEXT_CONSOLE_KEY_LEFT: 602 if (pos > 0) 603 pos--; 604 else if (scrollOffset > 0) { 605 scrollOffset--; 606 doScroll = true; 607 } 608 break; 609 case TEXT_CONSOLE_KEY_RIGHT: 610 if (pos < dataLength) { 611 if (x + (int32)pos == console_width() - 1) { 612 scrollOffset++; 613 doScroll = true; 614 } else 615 pos++; 616 } 617 break; 618 default: 619 break; 620 } 621 } else if (key == TEXT_CONSOLE_KEY_BACKSPACE) { 622 if (pos != 0 || scrollOffset > 0) { 623 if (pos > 0) 624 pos--; 625 else if (scrollOffset > 0) 626 scrollOffset--; 627 dataLength--; 628 int32 offset = pos + scrollOffset; 629 memmove(buffer + offset, buffer + offset + 1, dataLength - offset); 630 console_set_cursor(x + pos, line); 631 putchar(' '); 632 // if this was a mid-line backspace, the line will need to be redrawn 633 if (pos + scrollOffset < dataLength) 634 doScroll = true; 635 } 636 // only accept printable ascii characters 637 } else if (key > 32 || key == TEXT_CONSOLE_KEY_SPACE) { 638 if (pos < (bufferSize - 1)) { 639 buffer[pos + scrollOffset] = key; 640 if (x + (int32)pos < console_width() - 1) { 641 putchar(key); 642 pos++; 643 } else { 644 scrollOffset++; 645 doScroll = true; 646 } 647 648 dataLength++; 649 } 650 } 651 652 if (doScroll) { 653 console_set_cursor(x, line); 654 for (int32 i = x; i < console_width() - 1; i++) 655 putchar(buffer[scrollOffset + i - x]); 656 doScroll = false; 657 } 658 console_set_cursor(x + pos, line); 659 } 660 661 console_hide_cursor(); 662 draw_menu(menu); 663 664 return key == TEXT_CONSOLE_KEY_RETURN ? pos : 0; 665 } 666