1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: FreeLoader
4 * FILE: boot/freeldr/freeldr/ui/tuimenu.c
5 * PURPOSE: Text UI Menu Functions
6 * PROGRAMMERS: Alex Ionescu (alex@relsoft.net)
7 * Brian Palmer (brianp@sginet.com)
8 */
9
10 /* INCLUDES ******************************************************************/
11
12 #include <freeldr.h>
13
14 /* FUNCTIONS *****************************************************************/
15
16 static VOID
17 TuiCalcMenuBoxSize(
18 _In_ PUI_MENU_INFO MenuInfo);
19
20 static ULONG
21 TuiProcessMenuKeyboardEvent(
22 _In_ PUI_MENU_INFO MenuInfo,
23 _In_ UiMenuKeyPressFilterCallback KeyPressFilter);
24
25 static VOID
26 TuiDrawMenuTimeout(
27 _In_ PUI_MENU_INFO MenuInfo);
28
29 BOOLEAN
TuiDisplayMenu(IN PCSTR MenuHeader,IN PCSTR MenuFooter OPTIONAL,IN BOOLEAN ShowBootOptions,IN PCSTR MenuItemList[],IN ULONG MenuItemCount,IN ULONG DefaultMenuItem,IN LONG MenuTimeOut,OUT PULONG SelectedMenuItem,IN BOOLEAN CanEscape,IN UiMenuKeyPressFilterCallback KeyPressFilter OPTIONAL,IN PVOID Context OPTIONAL)30 TuiDisplayMenu(
31 IN PCSTR MenuHeader,
32 IN PCSTR MenuFooter OPTIONAL,
33 IN BOOLEAN ShowBootOptions,
34 IN PCSTR MenuItemList[],
35 IN ULONG MenuItemCount,
36 IN ULONG DefaultMenuItem,
37 IN LONG MenuTimeOut,
38 OUT PULONG SelectedMenuItem,
39 IN BOOLEAN CanEscape,
40 IN UiMenuKeyPressFilterCallback KeyPressFilter OPTIONAL,
41 IN PVOID Context OPTIONAL)
42 {
43 UI_MENU_INFO MenuInformation;
44 ULONG LastClockSecond;
45 ULONG CurrentClockSecond;
46 ULONG KeyPress;
47
48 /*
49 * Before taking any default action if there is no timeout,
50 * check whether the supplied key filter callback function
51 * may handle a specific user keypress. If it does, the
52 * timeout is cancelled.
53 */
54 if (!MenuTimeOut && KeyPressFilter && MachConsKbHit())
55 {
56 /* Get the key (get the extended key if needed) */
57 KeyPress = MachConsGetCh();
58 if (KeyPress == KEY_EXTENDED)
59 KeyPress = MachConsGetCh();
60
61 /*
62 * Call the supplied key filter callback function to see
63 * if it is going to handle this keypress.
64 */
65 if (KeyPressFilter(KeyPress, DefaultMenuItem, Context))
66 {
67 /* It processed the key character, cancel the timeout */
68 MenuTimeOut = -1;
69 }
70 }
71
72 /* Check if there is no timeout */
73 if (!MenuTimeOut)
74 {
75 /* Return the default selected item */
76 if (SelectedMenuItem) *SelectedMenuItem = DefaultMenuItem;
77 return TRUE;
78 }
79
80 /* Setup the MENU_INFO structure */
81 MenuInformation.MenuHeader = MenuHeader;
82 MenuInformation.MenuFooter = MenuFooter;
83 MenuInformation.ShowBootOptions = ShowBootOptions;
84 MenuInformation.MenuItemList = MenuItemList;
85 MenuInformation.MenuItemCount = MenuItemCount;
86 MenuInformation.MenuTimeRemaining = MenuTimeOut;
87 MenuInformation.SelectedMenuItem = DefaultMenuItem;
88 MenuInformation.Context = Context;
89
90 /* Calculate the size of the menu box */
91 TuiCalcMenuBoxSize(&MenuInformation);
92
93 /* Draw the menu */
94 UiVtbl.DrawMenu(&MenuInformation);
95
96 /* Get the current second of time */
97 LastClockSecond = ArcGetTime()->Second;
98
99 /* Process keys */
100 while (TRUE)
101 {
102 /* Process key presses */
103 KeyPress = TuiProcessMenuKeyboardEvent(&MenuInformation, KeyPressFilter);
104
105 /* Check for ENTER or ESC */
106 if (KeyPress == KEY_ENTER) break;
107 if (CanEscape && KeyPress == KEY_ESC) return FALSE;
108
109 /* Get the updated time, and check if more than a second has elapsed */
110 CurrentClockSecond = ArcGetTime()->Second;
111 if (CurrentClockSecond != LastClockSecond)
112 {
113 /* Update the time information */
114 LastClockSecond = CurrentClockSecond;
115
116 // FIXME: Theme-specific
117 /* Update the date & time */
118 TuiUpdateDateTime();
119
120 /* If there is a countdown, update it */
121 if (MenuInformation.MenuTimeRemaining > 0)
122 {
123 MenuInformation.MenuTimeRemaining--;
124 TuiDrawMenuTimeout(&MenuInformation);
125 }
126 else if (MenuInformation.MenuTimeRemaining == 0)
127 {
128 /* A timeout occurred, exit this loop and return selection */
129 VideoCopyOffScreenBufferToVRAM();
130 break;
131 }
132 VideoCopyOffScreenBufferToVRAM();
133 }
134
135 MachHwIdle();
136 }
137
138 /* Return the selected item */
139 if (SelectedMenuItem) *SelectedMenuItem = MenuInformation.SelectedMenuItem;
140 return TRUE;
141 }
142
143 static VOID
TuiCalcMenuBoxSize(_In_ PUI_MENU_INFO MenuInfo)144 TuiCalcMenuBoxSize(
145 _In_ PUI_MENU_INFO MenuInfo)
146 {
147 ULONG i;
148 ULONG Width = 0;
149 ULONG Height;
150 ULONG Length;
151
152 /* Height is the menu item count plus 2 (top border & bottom border) */
153 Height = MenuInfo->MenuItemCount + 2;
154
155 /* Loop every item */
156 for (i = 0; i < MenuInfo->MenuItemCount; ++i)
157 {
158 /* Get the string length and make it become the new width if necessary */
159 if (MenuInfo->MenuItemList[i])
160 {
161 Length = (ULONG)strlen(MenuInfo->MenuItemList[i]);
162 Width = max(Width, Length);
163 }
164 }
165
166 /* Allow room for left & right borders, plus 4 spaces on each side */
167 Width += 10;
168
169 /* Check if we're drawing a centered menu */
170 if (UiCenterMenu)
171 {
172 /* Calculate the centered menu box area, also ensuring that the top-left
173 * corner is always visible if the borders are partly off-screen */
174 MenuInfo->Left = (UiScreenWidth - min(Width, UiScreenWidth)) / 2;
175 if (Height <= UiScreenHeight - TUI_TITLE_BOX_CHAR_HEIGHT - 1)
176 {
177 /* Exclude the header and the status bar */
178 // MenuInfo->Top = (UiScreenHeight - TUI_TITLE_BOX_CHAR_HEIGHT - 1 - Height) / 2
179 // + TUI_TITLE_BOX_CHAR_HEIGHT;
180 MenuInfo->Top = (UiScreenHeight + TUI_TITLE_BOX_CHAR_HEIGHT - 1 - Height) / 2;
181 }
182 else
183 {
184 MenuInfo->Top = (UiScreenHeight - min(Height, UiScreenHeight)) / 2;
185 }
186 }
187 else
188 {
189 /* Put the menu in the default left-corner position */
190 MenuInfo->Left = -1;
191 MenuInfo->Top = 4;
192 }
193
194 /* The other margins are the same */
195 MenuInfo->Right = MenuInfo->Left + Width - 1;
196 MenuInfo->Bottom = MenuInfo->Top + Height - 1;
197 }
198
199 VOID
TuiDrawMenu(_In_ PUI_MENU_INFO MenuInfo)200 TuiDrawMenu(
201 _In_ PUI_MENU_INFO MenuInfo)
202 {
203 ULONG i;
204
205 // FIXME: Theme-specific
206 /* Draw the backdrop */
207 UiDrawBackdrop();
208
209 /* Draw the menu box */
210 TuiDrawMenuBox(MenuInfo);
211
212 /* Draw each line of the menu */
213 for (i = 0; i < MenuInfo->MenuItemCount; ++i)
214 {
215 TuiDrawMenuItem(MenuInfo, i);
216 }
217
218 // FIXME: Theme-specific
219 /* Update the status bar */
220 UiVtbl.DrawStatusText("Use \x18 and \x19 to select, then press ENTER.");
221
222 /* Display the boot options if needed */
223 if (MenuInfo->ShowBootOptions)
224 {
225 DisplayBootTimeOptions();
226 }
227
228 VideoCopyOffScreenBufferToVRAM();
229 }
230
231 static VOID
TuiDrawMenuTimeout(_In_ PUI_MENU_INFO MenuInfo)232 TuiDrawMenuTimeout(
233 _In_ PUI_MENU_INFO MenuInfo)
234 {
235 ULONG Length;
236 CHAR MenuLineText[80];
237
238 /* If there is a timeout, draw the time remaining */
239 if (MenuInfo->MenuTimeRemaining >= 0)
240 {
241 /* Find whether the time text string is escaped
242 * with %d for specific countdown insertion. */
243 PCHAR ptr = UiTimeText;
244 while ((ptr = strchr(ptr, '%')) && (ptr[1] != 'd'))
245 {
246 /* Ignore any following character (including a following
247 * '%' that would be escaped), thus skip two characters.
248 * If this is the last character, ignore it and stop. */
249 if (*++ptr)
250 ++ptr;
251 }
252 ASSERT(!ptr || (ptr[0] == '%' && ptr[1] == 'd'));
253
254 if (ptr)
255 {
256 /* Copy the time text string up to the '%d' insertion point and
257 * skip it, add the remaining time and the rest of the string. */
258 RtlStringCbPrintfA(MenuLineText, sizeof(MenuLineText),
259 "%.*s%d%s",
260 ptr - UiTimeText, UiTimeText,
261 MenuInfo->MenuTimeRemaining,
262 ptr + 2);
263 }
264 else
265 {
266 /* Copy the time text string, append a separating blank,
267 * and add the remaining time. */
268 RtlStringCbPrintfA(MenuLineText, sizeof(MenuLineText),
269 "%s %d",
270 UiTimeText,
271 MenuInfo->MenuTimeRemaining);
272 }
273
274 Length = (ULONG)strlen(MenuLineText);
275 }
276 else
277 {
278 /* Erase the timeout with blanks */
279 Length = 0;
280 }
281
282 /**
283 * How to pad/fill:
284 *
285 * Center Box What to do:
286 * 0 0 or 1 Pad on the right with blanks.
287 * 1 0 Pad on the left with blanks.
288 * 1 1 Pad on the left with blanks + box bottom border.
289 **/
290
291 if (UiCenterMenu)
292 {
293 /* In boxed menu mode, pad on the left with blanks and box border,
294 * otherwise, pad over all the box length until its right edge. */
295 TuiFillArea(0,
296 MenuInfo->Bottom,
297 UiMenuBox
298 ? MenuInfo->Left - 1 /* Left side of the box bottom */
299 : MenuInfo->Right, /* Left side + all box length */
300 MenuInfo->Bottom,
301 UiBackdropFillStyle,
302 ATTR(UiBackdropFgColor, UiBackdropBgColor));
303
304 if (UiMenuBox)
305 {
306 /* Fill with box bottom border */
307 TuiDrawBoxBottomLine(MenuInfo->Left,
308 MenuInfo->Bottom,
309 MenuInfo->Right,
310 D_VERT,
311 D_HORZ,
312 ATTR(UiMenuFgColor, UiMenuBgColor));
313
314 /* In centered boxed menu mode, the timeout string
315 * does not go past the right border, in principle... */
316 }
317
318 if (Length > 0)
319 {
320 /* Display the timeout at the bottom-right part of the menu */
321 UiDrawText(MenuInfo->Right - Length - 1,
322 MenuInfo->Bottom,
323 MenuLineText,
324 ATTR(UiMenuFgColor, UiMenuBgColor));
325 }
326 }
327 else
328 {
329 if (Length > 0)
330 {
331 /* Display the timeout under the menu directly */
332 UiDrawText(0,
333 MenuInfo->Bottom + 4,
334 MenuLineText,
335 ATTR(UiMenuFgColor, UiMenuBgColor));
336 }
337
338 /* Pad on the right with blanks, to erase
339 * characters when the string length decreases. */
340 TuiFillArea(Length,
341 MenuInfo->Bottom + 4,
342 Length ? (Length + 1) : (UiScreenWidth - 1),
343 MenuInfo->Bottom + 4,
344 UiBackdropFillStyle,
345 ATTR(UiBackdropFgColor, UiBackdropBgColor)
346 );
347 }
348 }
349
350 VOID
TuiDrawMenuBox(_In_ PUI_MENU_INFO MenuInfo)351 TuiDrawMenuBox(
352 _In_ PUI_MENU_INFO MenuInfo)
353 {
354 // FIXME: Theme-specific
355 /* Draw the menu box if requested */
356 if (UiMenuBox)
357 {
358 UiDrawBox(MenuInfo->Left,
359 MenuInfo->Top,
360 MenuInfo->Right,
361 MenuInfo->Bottom,
362 D_VERT,
363 D_HORZ,
364 FALSE, // Filled
365 TRUE, // Shadow
366 ATTR(UiMenuFgColor, UiMenuBgColor));
367 }
368
369 /* Update the date & time */
370 TuiUpdateDateTime();
371 TuiDrawMenuTimeout(MenuInfo);
372 }
373
374 VOID
TuiDrawMenuItem(_In_ PUI_MENU_INFO MenuInfo,_In_ ULONG MenuItemNumber)375 TuiDrawMenuItem(
376 _In_ PUI_MENU_INFO MenuInfo,
377 _In_ ULONG MenuItemNumber)
378 {
379 ULONG SpaceLeft;
380 ULONG SpaceRight;
381 UCHAR Attribute;
382 CHAR MenuLineText[80];
383
384 /* If this is a separator */
385 if (MenuInfo->MenuItemList[MenuItemNumber] == NULL)
386 {
387 // FIXME: Theme-specific
388 /* Draw its left box corner */
389 if (UiMenuBox)
390 {
391 UiDrawText(MenuInfo->Left,
392 MenuInfo->Top + 1 + MenuItemNumber,
393 "\xC7",
394 ATTR(UiMenuFgColor, UiMenuBgColor));
395 }
396
397 /* Make it a separator line and use menu colors */
398 RtlZeroMemory(MenuLineText, sizeof(MenuLineText));
399 RtlFillMemory(MenuLineText,
400 min(sizeof(MenuLineText), (MenuInfo->Right - MenuInfo->Left - 1)),
401 0xC4);
402
403 /* Draw the item */
404 UiDrawText(MenuInfo->Left + 1,
405 MenuInfo->Top + 1 + MenuItemNumber,
406 MenuLineText,
407 ATTR(UiMenuFgColor, UiMenuBgColor));
408
409 // FIXME: Theme-specific
410 /* Draw its right box corner */
411 if (UiMenuBox)
412 {
413 UiDrawText(MenuInfo->Right,
414 MenuInfo->Top + 1 + MenuItemNumber,
415 "\xB6",
416 ATTR(UiMenuFgColor, UiMenuBgColor));
417 }
418
419 /* We are done */
420 return;
421 }
422
423 /* This is not a separator */
424 ASSERT(MenuInfo->MenuItemList[MenuItemNumber]);
425
426 /* Check if using centered menu */
427 if (UiCenterMenu)
428 {
429 /*
430 * We will want the string centered so calculate
431 * how many spaces will be to the left and right.
432 */
433 ULONG SpaceTotal =
434 (MenuInfo->Right - MenuInfo->Left - 2) -
435 (ULONG)strlen(MenuInfo->MenuItemList[MenuItemNumber]);
436 SpaceLeft = (SpaceTotal / 2) + 1;
437 SpaceRight = (SpaceTotal - SpaceLeft) + 1;
438 }
439 else
440 {
441 /* Simply left-align it */
442 SpaceLeft = 4;
443 SpaceRight = 0;
444 }
445
446 /* Format the item text string */
447 RtlStringCbPrintfA(MenuLineText, sizeof(MenuLineText),
448 "%*s%s%*s",
449 SpaceLeft, "", // Left padding
450 MenuInfo->MenuItemList[MenuItemNumber],
451 SpaceRight, ""); // Right padding
452
453 if (MenuItemNumber == MenuInfo->SelectedMenuItem)
454 {
455 /* If this is the selected item, use the selected colors */
456 Attribute = ATTR(UiSelectedTextColor, UiSelectedTextBgColor);
457 }
458 else
459 {
460 /* Normal item colors */
461 Attribute = ATTR(UiTextColor, UiMenuBgColor);
462 }
463
464 /* Draw the item */
465 UiDrawText(MenuInfo->Left + 1,
466 MenuInfo->Top + 1 + MenuItemNumber,
467 MenuLineText,
468 Attribute);
469 }
470
471 static ULONG
TuiProcessMenuKeyboardEvent(_In_ PUI_MENU_INFO MenuInfo,_In_ UiMenuKeyPressFilterCallback KeyPressFilter)472 TuiProcessMenuKeyboardEvent(
473 _In_ PUI_MENU_INFO MenuInfo,
474 _In_ UiMenuKeyPressFilterCallback KeyPressFilter)
475 {
476 ULONG KeyEvent = 0;
477 ULONG Selected, Count;
478
479 /* Check for a keypress */
480 if (!MachConsKbHit())
481 return 0; // None, bail out
482
483 /* Check if the timeout is not already complete */
484 if (MenuInfo->MenuTimeRemaining != -1)
485 {
486 /* Cancel it and remove it */
487 MenuInfo->MenuTimeRemaining = -1;
488 TuiDrawMenuTimeout(MenuInfo);
489 }
490
491 /* Get the key (get the extended key if needed) */
492 KeyEvent = MachConsGetCh();
493 if (KeyEvent == KEY_EXTENDED)
494 KeyEvent = MachConsGetCh();
495
496 /*
497 * Call the supplied key filter callback function to see
498 * if it is going to handle this keypress.
499 */
500 if (KeyPressFilter &&
501 KeyPressFilter(KeyEvent, MenuInfo->SelectedMenuItem, MenuInfo->Context))
502 {
503 /* It processed the key character, so redraw and exit */
504 UiVtbl.DrawMenu(MenuInfo);
505 return 0;
506 }
507
508 /* Process the key */
509 if ((KeyEvent == KEY_UP ) || (KeyEvent == KEY_DOWN) ||
510 (KeyEvent == KEY_HOME) || (KeyEvent == KEY_END ))
511 {
512 /* Get the current selected item and count */
513 Selected = MenuInfo->SelectedMenuItem;
514 Count = MenuInfo->MenuItemCount - 1;
515
516 /* Check the key and change the selected menu item */
517 if ((KeyEvent == KEY_UP) && (Selected > 0))
518 {
519 /* Deselect previous item and go up */
520 MenuInfo->SelectedMenuItem--;
521 TuiDrawMenuItem(MenuInfo, Selected);
522 Selected--;
523
524 /* Skip past any separators */
525 if ((Selected > 0) &&
526 (MenuInfo->MenuItemList[Selected] == NULL))
527 {
528 MenuInfo->SelectedMenuItem--;
529 }
530 }
531 else if ( ((KeyEvent == KEY_UP) && (Selected == 0)) ||
532 (KeyEvent == KEY_END) )
533 {
534 /* Go to the end */
535 MenuInfo->SelectedMenuItem = Count;
536 TuiDrawMenuItem(MenuInfo, Selected);
537 }
538 else if ((KeyEvent == KEY_DOWN) && (Selected < Count))
539 {
540 /* Deselect previous item and go down */
541 MenuInfo->SelectedMenuItem++;
542 TuiDrawMenuItem(MenuInfo, Selected);
543 Selected++;
544
545 /* Skip past any separators */
546 if ((Selected < Count) &&
547 (MenuInfo->MenuItemList[Selected] == NULL))
548 {
549 MenuInfo->SelectedMenuItem++;
550 }
551 }
552 else if ( ((KeyEvent == KEY_DOWN) && (Selected == Count)) ||
553 (KeyEvent == KEY_HOME) )
554 {
555 /* Go to the beginning */
556 MenuInfo->SelectedMenuItem = 0;
557 TuiDrawMenuItem(MenuInfo, Selected);
558 }
559
560 /* Select new item and update video buffer */
561 TuiDrawMenuItem(MenuInfo, MenuInfo->SelectedMenuItem);
562 VideoCopyOffScreenBufferToVRAM();
563 }
564
565 /* Return the pressed key */
566 return KeyEvent;
567 }
568