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[width + 1]; 163 buffer[width] = '\0'; 164 // make sure the buffer is always terminated 165 166 int32 row = 0; 167 168 for (int32 i = 0; i < length && row < 2; i++) { 169 while (text[i] == ' ') 170 i++; 171 172 // copy as much bytes as possible 173 int32 bytes = width; 174 if (bytes > length - i) 175 bytes = length - i; 176 177 memcpy(buffer, text + i, bytes); 178 buffer[bytes] = '\0'; 179 180 char *pos = strchr(buffer, '\n'); 181 if (pos != NULL) 182 bytes = pos - buffer; 183 else if (bytes < length - i) { 184 // search for possible line breaks 185 pos = strrchr(buffer, ' '); 186 if (pos != NULL) 187 bytes = pos - buffer; 188 else { 189 // no wrapping possible 190 } 191 } 192 193 i += bytes; 194 buffer[bytes] = '\0'; 195 print_centered(console_height() - kHelpLines + row, buffer); 196 row++; 197 } 198 } 199 } 200 201 202 static void 203 draw_menu(Menu *menu) 204 { 205 console_set_color(kTextColor, kBackgroundColor); 206 console_clear_screen(); 207 208 print_centered(1, "Welcome to the"); 209 print_centered(2, "Haiku Boot Loader"); 210 211 console_set_color(kCopyrightColor, kBackgroundColor); 212 print_centered(4, "Copyright 2004-2020 Haiku, Inc."); 213 214 if (menu->Title()) { 215 console_set_cursor(kOffsetX, kFirstLine - 2); 216 console_set_color(kTitleColor, kTitleBackgroundColor); 217 218 printf(" %s", menu->Title()); 219 print_spacing(console_width() - 1 220 - strlen(menu->Title()) - 2 * kOffsetX); 221 } 222 223 MenuItemIterator iterator = menu->ItemIterator(); 224 MenuItem *item; 225 int32 i = 0; 226 227 while ((item = iterator.Next()) != NULL) { 228 if (item->Type() == MENU_ITEM_SEPARATOR) { 229 putchar('\n'); 230 i++; 231 continue; 232 } 233 234 print_item_at(i++, item, false); 235 } 236 237 int32 height = menu_height(); 238 if (menu->CountItems() >= height) { 239 int32 x = console_width() - kOffsetX; 240 console_set_cursor(x, kFirstLine); 241 console_set_color(kArrowColor, kBackgroundColor); 242 putchar(30/*24*/); 243 height--; 244 245 int32 start = sMenuOffset * height / menu->CountItems(); 246 int32 end = (sMenuOffset + height) * height / menu->CountItems(); 247 248 for (i = 1; i < height; i++) { 249 console_set_cursor(x, kFirstLine + i); 250 if (i >= start && i <= end) 251 console_set_color(WHITE, kSliderColor); 252 else 253 console_set_color(WHITE, kSliderBackgroundColor); 254 255 putchar(' '); 256 } 257 258 console_set_cursor(x, kFirstLine + i); 259 console_set_color(kArrowColor, kBackgroundColor); 260 putchar(31/*25*/); 261 } 262 } 263 264 265 static int32 266 first_selectable_item(Menu *menu) 267 { 268 int32 index = -1; 269 MenuItem *item; 270 271 while ((item = menu->ItemAt(++index)) != NULL) { 272 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 273 break; 274 } 275 276 return index; 277 } 278 279 280 static int32 281 last_selectable_item(Menu *menu) 282 { 283 int32 index = menu->CountItems(); 284 MenuItem *item; 285 286 while ((item = menu->ItemAt(--index)) != NULL) { 287 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 288 break; 289 } 290 291 return index; 292 } 293 294 295 static bool 296 make_item_visible(Menu *menu, int32 selected) 297 { 298 if (sMenuOffset > selected 299 || sMenuOffset + menu_height() <= selected) { 300 if (sMenuOffset > selected) 301 sMenuOffset = selected; 302 else 303 sMenuOffset = selected + 1 - menu_height(); 304 305 draw_menu(menu); 306 return true; 307 } 308 309 return false; 310 } 311 312 313 static int32 314 select_previous_valid_item(Menu *menu, int32 selected) 315 { 316 MenuItem *item; 317 while ((item = menu->ItemAt(selected)) != NULL) { 318 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 319 break; 320 321 selected--; 322 } 323 324 if (selected < 0) 325 return first_selectable_item(menu); 326 327 return selected; 328 } 329 330 331 static int32 332 select_next_valid_item(Menu *menu, int32 selected) 333 { 334 MenuItem *item; 335 while ((item = menu->ItemAt(selected)) != NULL) { 336 if (item->IsEnabled() && item->Type() != MENU_ITEM_SEPARATOR) 337 break; 338 339 selected++; 340 } 341 342 if (selected >= menu->CountItems()) 343 return last_selectable_item(menu); 344 345 return selected; 346 } 347 348 349 static bool 350 invoke_item(Menu* menu, MenuItem* item, int32& selected, char key) 351 { 352 // leave the menu 353 if (item->Submenu() != NULL && key == TEXT_CONSOLE_KEY_RETURN) { 354 int32 offset = sMenuOffset; 355 menu->Hide(); 356 357 run_menu(item->Submenu()); 358 if (item->Target() != NULL) 359 (*item->Target())(menu, item); 360 361 // restore current menu 362 sMenuOffset = offset; 363 menu->FindSelected(&selected); 364 menu->Show(); 365 draw_menu(menu); 366 } else if (item->Type() == MENU_ITEM_MARKABLE) { 367 // toggle state 368 item->SetMarked(!item->IsMarked()); 369 print_item_at(selected, item); 370 371 if (item->Target() != NULL) 372 (*item->Target())(menu, item); 373 } else if (key == TEXT_CONSOLE_KEY_RETURN) { 374 // the space key does not exit the menu, only return does 375 if (menu->Type() == CHOICE_MENU 376 && item->Type() != MENU_ITEM_NO_CHOICE 377 && item->Type() != MENU_ITEM_TITLE) 378 item->SetMarked(true); 379 380 if (item->Target() != NULL) 381 (*item->Target())(menu, item); 382 return true; 383 } 384 385 return false; 386 } 387 388 389 static void 390 run_menu(Menu* menu) 391 { 392 sMenuOffset = 0; 393 menu->Entered(); 394 menu->Show(); 395 396 draw_menu(menu); 397 398 // Get selected entry, or select the last one, if there is none 399 int32 selected; 400 MenuItem *item = menu->FindSelected(&selected); 401 if (item == NULL) { 402 selected = 0; 403 item = menu->ItemAt(selected); 404 if (item != NULL) 405 item->Select(true); 406 } 407 408 make_item_visible(menu, selected); 409 410 while (true) { 411 int key = console_wait_for_key(); 412 413 item = menu->ItemAt(selected); 414 415 if (TEXT_CONSOLE_IS_CURSOR_KEY(key) || key == 'j' || key == 'J' 416 || key == 'k' || key == 'K') { 417 if (item == NULL) 418 continue; 419 420 int32 oldSelected = selected; 421 422 switch (key) { 423 case TEXT_CONSOLE_KEY_UP: 424 case 'k': 425 case 'K': 426 selected = select_previous_valid_item(menu, selected - 1); 427 break; 428 case TEXT_CONSOLE_KEY_DOWN: 429 case 'j': 430 case 'J': 431 selected = select_next_valid_item(menu, selected + 1); 432 break; 433 case TEXT_CONSOLE_KEY_PAGE_UP: 434 case TEXT_CONSOLE_KEY_LEFT: 435 selected = select_previous_valid_item(menu, 436 selected - menu_height() + 1); 437 break; 438 case TEXT_CONSOLE_KEY_PAGE_DOWN: 439 case TEXT_CONSOLE_KEY_RIGHT: 440 selected = select_next_valid_item(menu, 441 selected + menu_height() - 1); 442 break; 443 case TEXT_CONSOLE_KEY_HOME: 444 selected = first_selectable_item(menu); 445 break; 446 case TEXT_CONSOLE_KEY_END: 447 selected = last_selectable_item(menu); 448 break; 449 } 450 451 // check if selected has changed 452 if (selected != oldSelected) { 453 MenuItem *item = menu->ItemAt(selected); 454 if (item != NULL) 455 item->Select(true); 456 457 make_item_visible(menu, selected); 458 // make sure that the new selected entry is visible 459 if (sMenuOffset > selected 460 || sMenuOffset + menu_height() <= selected) { 461 if (sMenuOffset > selected) 462 sMenuOffset = selected; 463 else 464 sMenuOffset = selected + 1 - menu_height(); 465 466 draw_menu(menu); 467 } 468 } 469 } else if (key == TEXT_CONSOLE_KEY_RETURN 470 || key == TEXT_CONSOLE_KEY_SPACE) { 471 if (item != NULL && invoke_item(menu, item, selected, key)) 472 break; 473 } else if (key == '\t') { 474 if (item == NULL) 475 continue; 476 477 int32 oldSelected = selected; 478 479 // Use tab to cycle between items (on some platforms, arrow keys 480 // are not available) 481 selected = select_next_valid_item(menu, selected + 1); 482 483 if (selected == oldSelected) 484 selected = first_selectable_item(menu); 485 486 // check if selected has changed 487 if (selected != oldSelected) { 488 MenuItem *item = menu->ItemAt(selected); 489 if (item != NULL) 490 item->Select(true); 491 492 make_item_visible(menu, selected); 493 // make sure that the new selected entry is visible 494 if (sMenuOffset > selected 495 || sMenuOffset + menu_height() <= selected) { 496 if (sMenuOffset > selected) 497 sMenuOffset = selected; 498 else 499 sMenuOffset = selected + 1 - menu_height(); 500 501 draw_menu(menu); 502 } 503 } 504 } else if (key == TEXT_CONSOLE_KEY_ESCAPE 505 && menu->Type() != MAIN_MENU) { 506 // escape key was hit 507 break; 508 } else { 509 // Shortcut processing 510 shortcut_hook function = menu->FindShortcut(key); 511 if (function != NULL) 512 function(key); 513 else { 514 item = menu->FindItemByShortcut(key); 515 if (item != NULL && invoke_item(menu, item, selected, 516 TEXT_CONSOLE_KEY_RETURN)) { 517 break; 518 } 519 } 520 } 521 } 522 523 menu->Hide(); 524 menu->Exited(); 525 } 526 527 528 // #pragma mark - 529 530 531 void 532 platform_generic_update_text_menu_item(Menu *menu, MenuItem *item) 533 { 534 if (menu->IsHidden()) 535 return; 536 537 int32 index = menu->IndexOf(item); 538 if (index == -1) 539 return; 540 541 print_item_at(index, item); 542 } 543 544 545 void 546 platform_generic_run_text_menu(Menu *menu) 547 { 548 // platform_switch_to_text_mode(); 549 550 run_menu(menu); 551 552 // platform_switch_to_logo(); 553 } 554 555 556 size_t 557 platform_generic_get_user_input_text(Menu* menu, MenuItem* item, char* buffer, 558 size_t bufferSize) 559 { 560 size_t pos = 0; 561 562 memset(buffer, 0, bufferSize); 563 564 int32 promptLength = strlen(item->Label()) + 2; 565 int32 line = menu->IndexOf(item) - sMenuOffset; 566 if (line < 0 || line >= menu_height()) 567 return 0; 568 569 line += kFirstLine; 570 console_set_cursor(kOffsetX, line); 571 int32 x = kOffsetX + 1; 572 console_set_cursor(0, line); 573 console_set_color(kSelectedItemColor, kSelectedItemBackgroundColor); 574 print_spacing(console_width()); 575 console_set_color(kTextColor, kBackgroundColor); 576 console_set_cursor(0, line); 577 print_spacing(x); 578 printf(item->Label()); 579 printf(": "); 580 x += promptLength; 581 console_set_color(kSelectedItemColor, kSelectedItemBackgroundColor); 582 console_show_cursor(); 583 console_set_cursor(x, line); 584 585 int32 scrollOffset = 0; 586 bool doScroll = false; 587 int key = 0; 588 size_t dataLength = 0; 589 while (true) { 590 key = console_wait_for_key(); 591 if (key == TEXT_CONSOLE_KEY_RETURN || key == TEXT_CONSOLE_KEY_ESCAPE) 592 break; 593 else if (key >= TEXT_CONSOLE_CURSOR_KEYS_START 594 && key < TEXT_CONSOLE_CURSOR_KEYS_END) 595 { 596 switch (key) { 597 case TEXT_CONSOLE_KEY_LEFT: 598 if (pos > 0) 599 pos--; 600 else if (scrollOffset > 0) { 601 scrollOffset--; 602 doScroll = true; 603 } 604 break; 605 case TEXT_CONSOLE_KEY_RIGHT: 606 if (pos < dataLength) { 607 if (x + (int32)pos == console_width() - 1) { 608 scrollOffset++; 609 doScroll = true; 610 } else 611 pos++; 612 } 613 break; 614 default: 615 break; 616 } 617 } else if (key == TEXT_CONSOLE_KEY_BACKSPACE) { 618 if (pos != 0 || scrollOffset > 0) { 619 if (pos > 0) 620 pos--; 621 else if (scrollOffset > 0) 622 scrollOffset--; 623 dataLength--; 624 int32 offset = pos + scrollOffset; 625 memmove(buffer + offset, buffer + offset + 1, dataLength - offset); 626 console_set_cursor(x + pos, line); 627 putchar(' '); 628 // if this was a mid-line backspace, the line will need to be redrawn 629 if (pos + scrollOffset < dataLength) 630 doScroll = true; 631 } 632 // only accept printable ascii characters 633 } else if (key > 32 || key == TEXT_CONSOLE_KEY_SPACE) { 634 if (pos < (bufferSize - 1)) { 635 buffer[pos + scrollOffset] = key; 636 if (x + (int32)pos < console_width() - 1) { 637 putchar(key); 638 pos++; 639 } else { 640 scrollOffset++; 641 doScroll = true; 642 } 643 644 dataLength++; 645 } 646 } 647 648 if (doScroll) { 649 console_set_cursor(x, line); 650 for (int32 i = x; i < console_width() - 1; i++) 651 putchar(buffer[scrollOffset + i - x]); 652 doScroll = false; 653 } 654 console_set_cursor(x + pos, line); 655 } 656 657 console_hide_cursor(); 658 draw_menu(menu); 659 660 return key == TEXT_CONSOLE_KEY_RETURN ? pos : 0; 661 } 662