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