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
18static const int32 kFirstLine = 8;
19static const int32 kOffsetX = 10;
20static const int32 kHelpLines = 3;
21
22// colors
23static const console_color kBackgroundColor = BLACK;
24static const console_color kTextColor = WHITE;
25static const console_color kCopyrightColor = CYAN;
26static const console_color kTitleColor = YELLOW;
27static const console_color kTitleBackgroundColor = kBackgroundColor;
28static const console_color kHelpTextColor = WHITE;
29
30static const console_color kItemColor = GRAY;
31static const console_color kSelectedItemColor = WHITE;
32static const console_color kItemBackgroundColor = kBackgroundColor;
33static const console_color kSelectedItemBackgroundColor = GRAY;
34static const console_color kDisabledColor = DARK_GRAY;
35
36static const console_color kSliderColor = CYAN;
37static const console_color kSliderBackgroundColor = DARK_GRAY;
38static const console_color kArrowColor = GRAY;
39
40static int32 sMenuOffset = 0;
41
42
43static void run_menu(Menu* menu);
44
45
46static int32
47menu_height()
48{
49	return console_height() - kFirstLine - 1 - kHelpLines;
50}
51
52
53static void
54print_spacing(int32 count)
55{
56	for (int32 i = 0; i < count; i++)
57		putchar(' ');
58}
59
60
61static void
62print_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
74static void
75print_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
87static void
88print_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
220static void
221draw_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
286static int32
287first_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
301static int32
302last_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
316static bool
317make_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
334static int32
335select_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
352static int32
353select_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
370static bool
371invoke_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
410static void
411run_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
552void
553platform_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
566void
567platform_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
577size_t
578platform_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