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