xref: /haiku/src/system/boot/platform/generic/text_menu.cpp (revision a5c0d1a80e18f50987966fda2005210092d7671b)
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