# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080728020230-a79jyq7pkv9jnco1 # target_branch: http://grub.gibibit.com/bzr/trunk-clean # testament_sha1: 990f6d3418364109d162680f19eb5aa7adddfadf # timestamp: 2008-07-27 19:16:45 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080728020030-u9gr2usazh2r2pcw # # Begin patch === modified file 'commands/videotest.c' --- commands/videotest.c 2008-07-09 16:14:04 +0000 +++ commands/videotest.c 2008-07-19 18:25:18 +0000 @@ -49,19 +49,6 @@ int double_buffering; }; -/* A 2D rectangle type. - * This could be worth integrating into the video API if it proves useful.*/ -struct grub_video_rect -{ - /* These are signed because if there are unsigned it causes Bad Things - * to happen when arithmetic and comparisions involving signed types is - * done. Important signed types include offsets from absolute locations. */ - int x; - int y; - int width; - int height; -}; -typedef struct grub_video_rect grub_video_rect_t; static void basic_video_test (struct videotest_options *vt_opts) === modified file 'conf/common.rmk' --- conf/common.rmk 2008-07-28 02:00:30 +0000 +++ conf/common.rmk 2008-07-28 02:02:30 +0000 @@ -274,6 +274,7 @@ cmp.mod cat.mod help.mod font.mod search.mod \ loopback.mod fs_uuid.mod configfile.mod echo.mod \ terminfo.mod test.mod blocklist.mod hexdump.mod \ + gfxmenu.mod \ read.mod sleep.mod loadenv.mod crc.mod # For hello.mod. @@ -281,6 +282,16 @@ hello_mod_CFLAGS = $(COMMON_CFLAGS) hello_mod_LDFLAGS = $(COMMON_LDFLAGS) +# For gfxmenu.mod. +gfxmenu_mod_SOURCES = \ + gfxmenu/gfxmenu.c \ + gfxmenu/model.c \ + gfxmenu/view.c \ + gfxmenu/widget-box.c \ + gfxmenu/stringutil.c +gfxmenu_mod_CFLAGS = $(COMMON_CFLAGS) +gfxmenu_mod_LDFLAGS = $(COMMON_LDFLAGS) + # For boot.mod. boot_mod_SOURCES = commands/boot.c boot_mod_CFLAGS = $(COMMON_CFLAGS) === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-07-28 02:00:30 +0000 +++ conf/i386-pc.rmk 2008-07-28 02:02:30 +0000 @@ -46,11 +46,13 @@ kern/i386/tsc.c \ kern/generic/millisleep.c \ kern/env.c \ + kern/menu_viewer.c \ term/i386/pc/console.c \ symlist.c kernel_img_HEADERS = arg.h boot.h cache.h device.h disk.h dl.h elf.h elfload.h \ env.h err.h file.h fs.h kernel.h loader.h misc.h mm.h net.h parser.h \ partition.h pc_partition.h rescue.h symbol.h term.h time.h types.h \ + menu_viewer.h \ machine/biosdisk.h machine/boot.h machine/console.h machine/init.h \ machine/memory.h machine/loader.h machine/vga.h machine/vbe.h machine/kernel.h kernel_img_CFLAGS = $(COMMON_CFLAGS) @@ -248,7 +250,7 @@ play_mod_LDFLAGS = $(COMMON_LDFLAGS) # For video.mod. -video_mod_SOURCES = video/video.c +video_mod_SOURCES = video/video.c video/setmode.c video_mod_CFLAGS = $(COMMON_CFLAGS) video_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'config.h.in' --- config.h.in 2008-07-13 00:55:15 +0000 +++ config.h.in 2008-07-19 21:46:40 +0000 @@ -113,10 +113,37 @@ /* Number of bits in a file offset, on hosts where this is settable. */ #undef _FILE_OFFSET_BITS +/* Define for large files, on AIX-style hosts. */ +#undef _LARGE_FILES + +/* Define to 1 if on MINIX. */ +#undef _MINIX + +/* Define to 2 if the system does not provide POSIX.1 features except with + this defined. */ +#undef _POSIX_1_SOURCE + +/* Define to 1 if you need to in order for `stat' and other things to work. */ +#undef _POSIX_SOURCE + +/* Enable extensions on AIX 3, Interix. */ +#ifndef _ALL_SOURCE +# undef _ALL_SOURCE +#endif /* Enable GNU extensions on systems that have them. */ #ifndef _GNU_SOURCE # undef _GNU_SOURCE #endif +/* Enable threading extensions on Solaris. */ +#ifndef _POSIX_PTHREAD_SEMANTICS +# undef _POSIX_PTHREAD_SEMANTICS +#endif +/* Enable extensions on HP NonStop. */ +#ifndef _TANDEM_SOURCE +# undef _TANDEM_SOURCE +#endif +/* Enable general extensions on Solaris. */ +#ifndef __EXTENSIONS__ +# undef __EXTENSIONS__ +#endif -/* Define for large files, on AIX-style hosts. */ -#undef _LARGE_FILES === added directory 'gfxmenu' === added file 'gfxmenu/gfxmenu.c' --- gfxmenu/gfxmenu.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gfxmenu.c 2008-07-27 00:06:20 +0000 @@ -0,0 +1,221 @@ +/* gfxmenu.c - Graphical menu interface controller. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void switch_to_text_menu (void) +{ + grub_env_set ("menuviewer", "terminal"); +} + +static void +process_key_press (int c, + grub_gfxmenu_model_t model, + grub_gfxmenu_view_t view, + int nested, + int *should_exit) +{ + /* When a key is pressed, stop the timeout. */ + grub_gfxmenu_model_clear_timeout (model); + + if (c == 'j' || c == GRUB_TERM_DOWN) + { + int i = grub_gfxmenu_model_get_selected_index (model); + int num_items = grub_gfxmenu_model_get_num_entries (model); + if (i < num_items - 1) + { + i++; + grub_gfxmenu_model_set_selected_index (model, i); + } + } + else if (c == 'k' || c == GRUB_TERM_UP) + { + int i = grub_gfxmenu_model_get_selected_index (model); + if (i > 0) + { + i--; + grub_gfxmenu_model_set_selected_index (model, i); + } + } + else if (c == '\r' || c == '\n' || c == GRUB_TERM_RIGHT) + { + int selected = grub_gfxmenu_model_get_selected_index (model); + int num_entries = grub_gfxmenu_model_get_num_entries (model); + if (selected >= 0 && selected < num_entries) + { + grub_menu_entry_t entry = + grub_gfxmenu_model_get_entry (model, selected); + grub_gfxmenu_view_execute_entry (view, entry); + } + } + else if (c == 'c') + { + grub_gfxmenu_view_run_terminal (view); + if (grub_errno != GRUB_ERR_NONE) + *should_exit = 1; + } + else if (c == 't') + { + /* The write hook for 'menuviewer' will cause + * grub_menu_viewer_should_return to return nonzero. */ + switch_to_text_menu (); + *should_exit = 1; + } + else if (c == '1') + { + grub_gfxmenu_view_load_theme (view, + "/boot/grub/themes/proto/theme.txt"); + } + else if (c == '2') + { + grub_gfxmenu_view_load_theme (view, + "/boot/grub/themes/winter/theme.txt"); + } + else if (nested && c == GRUB_TERM_ESC) + { + *should_exit = 1; + } +} + +static void +handle_key_events (grub_gfxmenu_model_t model, + grub_gfxmenu_view_t view, + int nested, + int *should_exit) +{ + while (!*should_exit && grub_checkkey () != -1) + { + int key = grub_getkey (); + int c = GRUB_TERM_ASCII_CHAR (key); + process_key_press (c, model, view, nested, should_exit); + } +} + +static grub_err_t +show_menu (grub_menu_t menu, int nested) +{ + grub_gfxmenu_model_t model; + + model = grub_gfxmenu_model_new (menu); + if (! model) + { + grub_print_error (); + grub_printf ("Initializing menu data for graphical menu failed;\n" + "falling back to terminal based menu.\n"); + grub_wait_after_message (); + switch_to_text_menu (); + return grub_errno; + } + + grub_gfxmenu_view_t view; + + /* Create the view. */ + const char *theme_path = grub_env_get ("theme"); + if (! theme_path) + theme_path = "/boot/grub/themes/proto/theme.txt"; + + view = grub_gfxmenu_view_new (theme_path, model); + if (! view) + { + grub_print_error (); + grub_printf ("Starting graphical menu failed;\n" + "falling back to terminal based menu.\n"); + grub_wait_after_message (); + grub_gfxmenu_model_destroy (model); + switch_to_text_menu (); + return grub_errno; + } + + /* Initially select the default menu entry. */ + int default_index = grub_menu_get_default_entry_index (menu); + grub_gfxmenu_model_set_selected_index (model, default_index); + + /* Start the timer to execute the default entry. */ + grub_gfxmenu_model_set_timeout (model); + + /* Main event loop. */ + int exit_requested = 0; + while (!exit_requested && !grub_menu_viewer_should_return ()) + { + if (grub_gfxmenu_model_timeout_expired (model)) + { + grub_gfxmenu_model_clear_timeout (model); + int i = grub_gfxmenu_model_get_selected_index (model); + grub_menu_entry_t e = grub_gfxmenu_model_get_entry (model, i); + grub_gfxmenu_view_execute_with_fallback (view, e); + continue; + } + + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + handle_key_events (model, view, nested, &exit_requested); + } + + grub_gfxmenu_view_destroy (view); + grub_gfxmenu_model_destroy (model); + + return grub_errno; +} + +static grub_err_t +grub_cmd_gfxmenu (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + grub_menu_t menu = grub_env_get_data_slot ("menu"); + if (!menu) + return grub_error (GRUB_ERR_MENU, "No menu context"); + + return show_menu (menu, 1); +} + +static struct grub_menu_viewer menu_viewer = +{ + .name = "gfxmenu", + .show_menu = show_menu +}; + +GRUB_MOD_INIT (gfxmenu) +{ + (void) mod; /* To stop warning. */ + grub_menu_viewer_register (&menu_viewer); + grub_register_command ("gfxmenu", + grub_cmd_gfxmenu, GRUB_COMMAND_FLAG_BOTH, + "gfxmenu", "Show graphical menu interface", 0); +} + +GRUB_MOD_FINI (gfxmenu) +{ + grub_unregister_command ("gfxmenu"); +} === added file 'gfxmenu/model.c' --- gfxmenu/model.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/model.c 2008-07-26 23:37:04 +0000 @@ -0,0 +1,192 @@ +/* model.c - Graphical menu interface MVC model. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Model type definition. */ +struct grub_gfxmenu_model +{ + grub_menu_t menu; + int num_entries; + grub_menu_entry_t *entries; + int selected_entry_index; + int timeout_set; + grub_uint64_t timeout_start; + grub_uint64_t timeout_at; +}; + + +grub_gfxmenu_model_t +grub_gfxmenu_model_new (grub_menu_t menu) +{ + grub_gfxmenu_model_t model; + + model = grub_malloc (sizeof (*model)); + if (! model) + return 0; + + model->menu = menu; + model->num_entries = menu->size; + model->entries = 0; + model->selected_entry_index = 0; + model->timeout_set = 0; + model->timeout_at = 0; + if (model->num_entries > 0) + { + model->entries = grub_malloc (model->num_entries + * sizeof (*model->entries)); + if (! model->entries) + goto fail_and_free; + + int i; + grub_menu_entry_t cur; + for (i = 0, cur = menu->entry_list; + i < model->num_entries; + i++, cur = cur->next) + { + model->entries[i] = cur; + } + } + + return model; + +fail_and_free: + grub_free (model->entries); + grub_free (model); + return 0; +} + +void +grub_gfxmenu_model_destroy (grub_gfxmenu_model_t model) +{ + if (! model) + return; + + grub_free (model->entries); + model->entries = 0; + + grub_free (model); +} + +grub_menu_t +grub_gfxmenu_model_get_menu (grub_gfxmenu_model_t model) +{ + return model->menu; +} + +void +grub_gfxmenu_model_set_timeout (grub_gfxmenu_model_t model) +{ + int timeout_sec = grub_menu_get_timeout (); + if (timeout_sec >= 0) + { + model->timeout_start = grub_get_time_ms (); + model->timeout_at = model->timeout_start + timeout_sec * 1000; + model->timeout_set = 1; + } + else + { + model->timeout_set = 0; + } +} + +void +grub_gfxmenu_model_clear_timeout (grub_gfxmenu_model_t model) +{ + model->timeout_set = 0; + grub_menu_set_timeout (-1); +} + +int +grub_gfxmenu_model_get_timeout_ms (grub_gfxmenu_model_t model) +{ + if (!model->timeout_set) + return -1; + + return model->timeout_at - model->timeout_start; +} + +int +grub_gfxmenu_model_get_timeout_remaining_ms (grub_gfxmenu_model_t model) +{ + if (!model->timeout_set) + return -1; + + return model->timeout_at - grub_get_time_ms (); +} + +int +grub_gfxmenu_model_timeout_expired (grub_gfxmenu_model_t model) +{ + if (model->timeout_set + && grub_get_time_ms () >= model->timeout_at) + return 1; + + return 0; +} + +int +grub_gfxmenu_model_get_num_entries (grub_gfxmenu_model_t model) +{ + return model->num_entries; +} + +int +grub_gfxmenu_model_get_selected_index (grub_gfxmenu_model_t model) +{ + return model->selected_entry_index; +} + +void +grub_gfxmenu_model_set_selected_index (grub_gfxmenu_model_t model, int index) +{ + model->selected_entry_index = index; +} + +const char * +grub_gfxmenu_model_get_entry_title (grub_gfxmenu_model_t model, int index) +{ + if (index < 0 || index >= model->num_entries) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "invalid menu index"); + return 0; + } + + return model->entries[index]->title; +} + +grub_menu_entry_t +grub_gfxmenu_model_get_entry (grub_gfxmenu_model_t model, int index) +{ + if (index < 0 || index >= model->num_entries) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "invalid menu index"); + return 0; + } + + return model->entries[index]; +} + === added file 'gfxmenu/stringutil.c' --- gfxmenu/stringutil.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/stringutil.c 2008-07-26 00:44:48 +0000 @@ -0,0 +1,196 @@ +/* stringutil.c - String utilities. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +/* Create a new NUL-terminated string on the heap as a substring of BUF. + The range of buf included is the half-open interval [START,END). + The index START is inclusive, END is exclusive. */ +char * +grub_new_substring (const char *buf, + grub_size_t start, grub_size_t end) +{ + char *s = grub_malloc (end - start + 1); + if (! s) + return 0; + grub_memcpy (s, buf + start, end - start); + s[end - start] = '\0'; + return s; +} + +/* Eliminate "." and ".." path elements from PATH. A new heap-allocated + string is returned. */ +static char * +canonicalize_path (const char *path) +{ + int i; + const char *p; + char *newpath = 0; + + /* Count the path components in path. */ + int components = 1; + for (p = path; *p; p++) + if (*p == '/') + components++; + + char **path_array = grub_malloc (components * sizeof (*path_array)); + if (! path_array) + return 0; + + /* Initialize array elements to NULL pointers; in case once of the + allocations fails, the cleanup code can just call grub_free() for all + pointers in the array. */ + for (i = 0; i < components; i++) + path_array[i] = 0; + + /* Parse the path into path_array. */ + p = path; + for (i = 0; i < components && p; i++) + { + /* Find the end of the path element. */ + const char *end = grub_strchr (p, '/'); + if (!end) + end = p + grub_strlen (p); + + /* Copy the element. */ + path_array[i] = grub_new_substring (p, 0, end - p); + if (!path_array[i]) + goto cleanup; + + /* Advance p to point to the start of the next element, or NULL. */ + if (*end) + p = end + 1; + else + p = 0; + } + + /* Eliminate '.' and '..' elements from the path array. */ + int newpath_length = 0; + for (i = components - 1; i >= 0; --i) + { + if (! grub_strcmp (path_array[i], ".")) + { + grub_free (path_array[i]); + path_array[i] = 0; + } + else if (! grub_strcmp (path_array[i], "..") + && i > 0) + { + /* Delete the '..' and the prior path element. */ + grub_free (path_array[i]); + path_array[i] = 0; + --i; + grub_free (path_array[i]); + path_array[i] = 0; + } + else + { + newpath_length += grub_strlen (path_array[i]) + 1; + } + } + + /* Construct a new path string. */ + newpath = grub_malloc (newpath_length + 1); + if (! newpath) + goto cleanup; + + newpath[0] = '\0'; + char *newpath_end = newpath; + int first = 1; + for (i = 0; i < components; i++) + { + char *element = path_array[i]; + if (element) + { + /* For all components but the first, prefix with a slash. */ + if (! first) + newpath_end = grub_stpcpy (newpath_end, "/"); + newpath_end = grub_stpcpy (newpath_end, element); + first = 0; + } + } + +cleanup: + for (i = 0; i < components; i++) + grub_free (path_array[i]); + grub_free (path_array); + + return newpath; +} + +/* Return a new heap-allocated string representing to absolute path + to the file referred to by PATH. If PATH is an absolute path, then + the returned path is a copy of PATH. If PATH is a relative path, then + BASE is with PATH used to construct the absolute path. */ +char * +grub_resolve_relative_path (const char *base, const char *path) +{ + char *abspath; + char *canonpath; + char *p; + + /* If PATH is an absolute path, then just use it as is. */ + if (path[0] == '/' || path[0] == '(') + return canonicalize_path (path); + + abspath = grub_malloc (grub_strlen (base) + grub_strlen (path) + 1); + if (!abspath) + return 0; + + /* Concatenate BASE and PATH. + Note that BASE is expected to have a trailing slash. */ + p = grub_stpcpy (abspath, base); + grub_stpcpy (p, path); + + canonpath = canonicalize_path (abspath); + if (!canonpath) + return abspath; + + grub_free (abspath); + return canonpath; +} + +/* Get the path of the directory where the file at FILE_PATH is located. + FILE_PATH should refer to a file, not a directory. The returned path + includes a trailing slash. + This does not handle GRUB "(hd0,0)" paths properly yet since it only + looks at slashes. */ +char * +grub_get_dirname (const char *file_path) +{ + int i; + int last_slash; + + last_slash = -1; + for (i = grub_strlen (file_path) - 1; i >= 0; --i) + { + if (file_path[i] == '/') + { + last_slash = i; + break; + } + } + if (last_slash == -1) + return grub_strdup ("/"); + + return grub_new_substring (file_path, 0, last_slash + 1); +} === added file 'gfxmenu/view.c' --- gfxmenu/view.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/view.c 2008-07-27 00:32:41 +0000 @@ -0,0 +1,959 @@ +/* view.c - Graphical menu interface MVC view. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Specifies a time (in ms) to delay so the user can read the message if a + default boot attempt fails and an attempt is made to fall back on + another entry. */ +#define FALLBACK_MESSAGE_DELAY 2000 + +/* Definition of the private representation of the view. */ +struct grub_gfxmenu_view +{ + grub_video_rect_t screen; + + int icon_width; + int icon_height; + int item_height; + int item_padding; + int item_icon_space; + int item_spacing; + grub_font_t title_font; + grub_font_t item_font; + grub_font_t selected_item_font; + grub_font_t status_font; + char *terminal_font_name; + grub_video_color_t title_color; + grub_video_color_t item_color; + grub_video_color_t selected_item_color; + grub_video_color_t status_color; + grub_video_color_t status_bg_color; + grub_video_color_t progress_bar_border_color; + grub_video_color_t progress_bar_fg_color; + grub_video_color_t progress_bar_bg_color; + struct grub_video_bitmap *desktop_image; + grub_video_color_t desktop_color; + grub_gfxmenu_box_t menu_box; + grub_gfxmenu_box_t selected_item_box; + grub_gfxmenu_box_t terminal_box; + char *title_text; + char *progress_message_text; + + grub_gfxmenu_model_t model; +}; + +static void init_terminal (grub_gfxmenu_view_t view); +static void destroy_terminal (void); +static grub_err_t set_graphics_mode (void); +static grub_err_t set_text_mode (void); + +/* Create a new view object, loading the theme specified by THEME_PATH and + associating MODEL with the view. */ +grub_gfxmenu_view_t +grub_gfxmenu_view_new (const char *theme_path, grub_gfxmenu_model_t model) +{ + grub_gfxmenu_view_t view; + + view = grub_malloc (sizeof (*view)); + if (! view) + return 0; + + set_graphics_mode (); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + grub_video_get_viewport ((unsigned *) &view->screen.x, + (unsigned *) &view->screen.y, + (unsigned *) &view->screen.width, + (unsigned *) &view->screen.height); + + /* Clear the screen; there may be garbage left over in video memory, and + loading the menu style (particularly the background) can take a while. */ + grub_video_fill_rect (grub_video_map_rgb (0, 0, 0), + view->screen.x, view->screen.y, + view->screen.width, view->screen.height); + grub_video_swap_buffers (); + + grub_font_t default_font; + grub_video_color_t default_fg_color; + grub_video_color_t default_bg_color; + + default_font = grub_font_get ("Helvetica 12"); + default_fg_color = grub_video_map_rgb (0, 0, 0); + default_bg_color = grub_video_map_rgb (255, 255, 255); + + view->icon_width = 32; + view->icon_height = 32; + view->item_height = 42; + view->item_padding = 14; + view->item_icon_space = 4; + view->item_spacing = 16; + view->title_font = default_font; + view->item_font = default_font; + view->selected_item_font = default_font; + view->status_font = default_font; + view->terminal_font_name = grub_strdup ("Fixed 10"); + view->title_color = default_fg_color; + view->item_color = default_fg_color; + view->selected_item_color = default_fg_color; + view->status_color = default_bg_color; + view->status_bg_color = default_fg_color; + view->progress_bar_border_color = default_fg_color; + view->progress_bar_fg_color = grub_video_map_rgb (160, 160, 160); + view->progress_bar_bg_color = default_bg_color; + view->desktop_image = 0; + view->desktop_color = default_bg_color; + view->menu_box = 0; + view->selected_item_box = 0; + view->terminal_box = 0; + view->title_text = grub_strdup ("GRUB Boot Menu"); + view->progress_message_text = 0; + + view->model = model; + + if (! grub_gfxmenu_view_load_theme (view, theme_path)) + { + grub_gfxmenu_view_destroy (view); + return 0; + } + + init_terminal (view); + + return view; +} + +/* Destroy the view object. All used memory is freed. */ +void +grub_gfxmenu_view_destroy (grub_gfxmenu_view_t view) +{ + grub_video_bitmap_destroy (view->desktop_image); + if (view->menu_box) + view->menu_box->destroy (view->menu_box); + if (view->selected_item_box) + view->selected_item_box->destroy (view->selected_item_box); + if (view->terminal_box) + view->terminal_box->destroy (view->terminal_box); + grub_free (view->terminal_font_name); + grub_free (view->title_text); + grub_free (view->progress_message_text); + grub_free (view); + + set_text_mode (); + destroy_terminal (); +} + +/* Sets MESSAGE as the progress message for the view. The string MESSAGE + must be heap-allocated and will be owned by VIEW. MESSAGE can be 0, in + which case no message is displayed. */ +static void +set_progress_message (grub_gfxmenu_view_t view, char *message) +{ + grub_free (view->progress_message_text); + view->progress_message_text = message; +} + +/* Parse a color string of the form "r, g, b". + Whitespace is insignificant. */ +static grub_video_color_t +parse_color (const char *s) +{ + int red = grub_strtoul (s, 0, 0); + if ((s = grub_strchr (s, ',')) == 0) + goto fail; + s++; + int green = grub_strtoul (s, 0, 0); + if ((s = grub_strchr (s, ',')) == 0) + goto fail; + s++; + int blue = grub_strtoul (s, 0, 0); + int alpha; + if ((s = grub_strchr (s, ',')) == 0) + alpha = 255; + else + { + s++; + alpha = grub_strtoul (s, 0, 0); + } + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + return grub_video_map_rgba (red, green, blue, alpha); + +fail: + return 0; +} + +/* Construct a new box widget using PATTERN to find the pixmap files for it, + storing the new widget at *BOXPTR. PATTERN should be of the form: + "somewhere/style*.png". The '*' then gets substituted with the various + pixmap names that the widget uses. + Return zero on failure, nonzero on success. */ +static int +theme_set_box (grub_gfxmenu_box_t *boxptr, + char *pattern, const char *theme_dir) +{ + char *abspattern; + char *prefix; + char *suffix; + char *star; + grub_gfxmenu_box_t box; + + abspattern = grub_resolve_relative_path (theme_dir, pattern); + if (! abspattern) + return 0; + + star = grub_strchr (abspattern, '*'); + if (! star) + { + grub_free (abspattern); + return 0; + } + + /* Prefix: Get the part before the '*'. */ + prefix = grub_malloc (star - abspattern + 1); + if (! prefix) + { + grub_free (abspattern); + return 0; + } + + grub_memcpy (prefix, abspattern, star - abspattern); + prefix[star - abspattern] = '\0'; + + /* Suffix: Everything after the '*' is the suffix. */ + suffix = star + 1; + + box = grub_gfxmenu_create_box (prefix, suffix); + grub_free (abspattern); /* Note: suffix, star point into abspattern. */ + grub_free (prefix); + if (! box) + return 0; + + if (*boxptr) + (*boxptr)->destroy (*boxptr); + *boxptr = box; + return 1; + +} + +/* Set the specified property NAME on the view to the given string VALUE. + This function takes ownership of both NAME and VALUE, so the caller + should pass pointers to new heap-allocated strings. */ +static void +theme_set_string (grub_gfxmenu_view_t view, char *name, char *value, + const char *theme_dir) +{ + if (! grub_strcmp ("title-font", name)) + view->title_font = grub_font_get (value); + else if (! grub_strcmp ("item-font", name)) + view->item_font = grub_font_get (value); + else if (! grub_strcmp ("selected-item-font", name)) + view->selected_item_font = grub_font_get (value); + else if (! grub_strcmp ("status-font", name)) + view->status_font = grub_font_get (value); + else if (! grub_strcmp ("terminal-font", name)) + { + grub_free (view->terminal_font_name); + view->terminal_font_name = value; + value = 0; /* Prevent value from being freed below. */ + } + else if (! grub_strcmp ("title-color", name)) + view->title_color = parse_color (value); + else if (! grub_strcmp ("item-color", name)) + view->item_color = parse_color (value); + else if (! grub_strcmp ("selected-item-color", name)) + view->selected_item_color = parse_color (value); + else if (! grub_strcmp ("status-color", name)) + view->status_color = parse_color (value); + else if (! grub_strcmp ("status-bg-color", name)) + view->status_bg_color = parse_color (value); + else if (! grub_strcmp ("progress-bar-border-color", name)) + view->progress_bar_border_color = parse_color (value); + else if (! grub_strcmp ("progress-bar-fg-color", name)) + view->progress_bar_fg_color = parse_color (value); + else if (! grub_strcmp ("progress-bar-bg-color", name)) + view->progress_bar_bg_color = parse_color (value); + else if (! grub_strcmp ("desktop-image", name)) + { + struct grub_video_bitmap *raw_bitmap; + struct grub_video_bitmap *scaled_bitmap; + char *path; + path = grub_resolve_relative_path (theme_dir, value); + if (! path) + goto fail; + if (grub_video_bitmap_load (&raw_bitmap, path) != GRUB_ERR_NONE) + { + grub_free (path); + goto fail; + } + grub_free(path); + grub_video_bitmap_create_scaled (&scaled_bitmap, + view->screen.width, + view->screen.height, + raw_bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + grub_video_bitmap_destroy (raw_bitmap); + if (!scaled_bitmap) + goto fail; + + grub_video_bitmap_destroy (view->desktop_image); + view->desktop_image = scaled_bitmap; + } + else if (! grub_strcmp ("desktop-color", name)) + view->desktop_color = parse_color (value); + else if (! grub_strcmp ("menu-box", name)) + theme_set_box (&view->menu_box, value, theme_dir); + else if (! grub_strcmp ("selected-item-box", name)) + theme_set_box (&view->selected_item_box, value, theme_dir); + else if (! grub_strcmp ("terminal-box", name)) + theme_set_box (&view->terminal_box, value, theme_dir); + else if (! grub_strcmp ("title-text", name)) + { + grub_free (view->title_text); + view->title_text = value; + value = 0; /* Prevent value from being freed below. */ + } + +fail: + grub_free (value); + grub_free (name); +} + +/* Set the specified property NAME on the view to the given numeric VALUE. + This function takes ownership NAME, so the caller should pass a pointer + to a new heap-allocated string. */ +static void +theme_set_number (grub_gfxmenu_view_t view, char *name, int value) +{ + if (! grub_strcmp ("icon-width", name)) + view->icon_width = value; + else if (! grub_strcmp ("icon-height", name)) + view->icon_height = value; + else if (! grub_strcmp ("item-height", name)) + view->item_height = value; + else if (! grub_strcmp ("item-padding", name)) + view->item_padding = value; + else if (! grub_strcmp ("item-icon-space", name)) + view->item_icon_space = value; + else if (! grub_strcmp ("item-spacing", name)) + view->item_spacing = value; + + grub_free (name); +} + +/* Set properties on the view based on settings from the specified + theme file. Returns nonzero on success, zero on failure. */ +int +grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, const char *theme_path) +{ + char *theme_dir; + grub_file_t file; + char *buf; + int pos; + int len; + + theme_dir = grub_get_dirname (theme_path); + + file = grub_file_open (theme_path); + if (!file) + { + grub_free (theme_dir); + return 0; + } + + len = grub_file_size (file); + buf = grub_malloc (len); + if (! buf) + { + grub_file_close (file); + grub_free (theme_dir); + return 0; + } + if (grub_file_read (file, buf, len) != len) + { + grub_free (buf); + grub_file_close (file); + grub_free (theme_dir); + return 0; + } + + pos = 0; + while (pos < len) + { + /* Skip comments (lines beginning with #). */ + if (pos < len && buf[pos] == '#') + goto nextline; + + /* Get name. */ + /* Find a word character. */ + while (pos < len && grub_isspace(buf[pos])) + pos++; + int name_start = pos; + /* Find the end of the name. */ + while (pos < len + && (grub_isalpha(buf[pos]) + || grub_isdigit(buf[pos]) + || buf[pos] == '_' + || buf[pos] == '-')) + pos++; + int name_end = pos; + + if (name_end - name_start < 1) + goto nextline; + + /* Skip whitespace before separator. */ + while (pos < len + && (buf[pos] == ' ' + || buf[pos] == '\t' + || buf[pos] == '\f')) + pos++; + + /* Read separator. */ + if (buf[pos] != ':') + goto nextline; + + pos++; /* Skip separator. */ + + /* Skip whitespace after separator. */ + while (pos < len + && (buf[pos] == ' ' + || buf[pos] == '\t' + || buf[pos] == '\f')) + pos++; + + /* Get the value based on its type. */ + if (pos < len && buf[pos] == '"') + { + /* String value. (e.g., '"My string"') */ + + int value_start; + int value_end; + + /* Skip the opening quotation mark. */ + pos++; + /* Get string value. */ + value_start = pos; + /* Find the ending quotation mark. */ + while (pos < len + && !(buf[pos] == '"' + || buf[pos] == '\n')) + pos++; + value_end = pos; + theme_set_string (view, + grub_new_substring (buf, name_start, name_end), + grub_new_substring (buf, value_start, value_end), + theme_dir); + } + else if (pos < len && grub_isdigit(buf[pos])) + { + /* Numeric value. (e.g., '123') */ + + int value_start; + int value_end; + char *value_str; + int value; + + /* Get numeric value. */ + value_start = pos; + /* Find the end of the digit sequence. */ + while (pos < len && grub_isdigit(buf[pos])) + pos++; + value_end = pos; + value_str = grub_new_substring (buf, value_start, value_end); + if (!value_str) + continue; + value = grub_strtoul (value_str, 0, 0); + grub_free (value_str); + theme_set_number (view, + grub_new_substring (buf, name_start, name_end), + value); + } + +nextline: + /* Eat characters up to the newline. */ + while (pos < len && buf[pos] != '\n') + pos++; + pos++; /* Eat the newline. */ + } + + grub_free (buf); + grub_file_close (file); + grub_free (theme_dir); + return 1; +} + +static void +draw_background (grub_gfxmenu_view_t view) +{ + if (view->desktop_image) + { + struct grub_video_bitmap *img = view->desktop_image; + grub_video_blit_bitmap (img, GRUB_VIDEO_BLIT_REPLACE, + view->screen.x, view->screen.y, 0, 0, + grub_video_bitmap_get_width (img), + grub_video_bitmap_get_height (img)); + } + else + { + grub_video_fill_rect (view->desktop_color, + view->screen.x, view->screen.y, + view->screen.width, view->screen.height); + } +} + +static struct grub_video_bitmap * +get_item_icon (grub_gfxmenu_view_t view __attribute__((unused)), + int item_index __attribute__((unused))) +{ + /* TODO Implement icons. */ + return 0; +} + +static void +draw_menu (grub_gfxmenu_view_t view) +{ + int boxpad = view->item_padding; + int icon_text_space = view->item_icon_space; + int item_vspace = view->item_spacing; + + int ascent = grub_font_get_ascent (view->item_font); + int descent = grub_font_get_descent (view->item_font); + int item_height = view->item_height; + + int num_items = grub_gfxmenu_model_get_num_entries (view->model); + grub_video_rect_t r; + r.width = view->screen.width * 4 / 5; + /* Set the menu box height to fit the items. */ + r.height = (item_height * num_items + + item_vspace * (num_items - 1) + + 2 * boxpad); + r.x = (view->screen.width - r.width) / 2; + r.y = (view->screen.height - r.height) / 2; + view->menu_box->set_content_size (view->menu_box, r.width, r.height); + + int menu_box_left_pad = view->menu_box->get_left_pad (view->menu_box); + int menu_box_top_pad = view->menu_box->get_top_pad (view->menu_box); + view->menu_box->draw (view->menu_box, + r.x - menu_box_left_pad, r.y - menu_box_top_pad); + + int item_top = r.y + boxpad; + int item_left = r.x + boxpad; + int i; + + for (i = 0; i < num_items; i++) + { + int is_selected = + (i == grub_gfxmenu_model_get_selected_index (view->model)); + + if (is_selected) + { + view->selected_item_box->set_content_size (view->selected_item_box, + r.width - 2 * boxpad, + item_height); + int leftpad = view->selected_item_box->get_left_pad (view->selected_item_box); + int toppad = view->selected_item_box->get_top_pad (view->selected_item_box); + view->selected_item_box->draw (view->selected_item_box, + item_left - leftpad, + item_top - toppad); + } + + struct grub_video_bitmap *icon; + if ((icon = get_item_icon (view, i)) != 0) + grub_video_blit_bitmap (icon, GRUB_VIDEO_BLIT_BLEND, + item_left, + item_top + (item_height - view->icon_height) / 2, + 0, 0, view->icon_width, view->icon_height); + + const char *item_title = + grub_gfxmenu_model_get_entry_title (view->model, i); + grub_video_draw_string (item_title, + (is_selected + ? view->selected_item_font + : view->item_font), + (is_selected + ? view->selected_item_color + : view->item_color), + item_left + view->icon_width + icon_text_space, + (item_top + (item_height - (ascent + descent)) + / 2 + ascent)); + + item_top += item_height + item_vspace; + } +} + +static void +draw_title (grub_gfxmenu_view_t view) +{ + if (! view->title_text) + return; + + /* Center the title. */ + int title_width = grub_font_get_string_width (view->title_font, + view->title_text); + int x = (view->screen.width - title_width) / 2; + int y = 40 + grub_font_get_ascent (view->title_font); + grub_video_draw_string (view->title_text, + view->title_font, view->title_color, + x, y); +} + +static void +draw_status (grub_gfxmenu_view_t view) +{ + int descent = grub_font_get_descent (view->status_font); + int ascent = grub_font_get_ascent (view->status_font); + int vpad = 5; + int textheight = descent + ascent + 1; + int h = 2 * vpad + 2 * textheight; + + grub_video_fill_rect (view->status_bg_color, + 0, view->screen.height - h, + view->screen.width, view->screen.height - 1); + + int texty = view->screen.height - h + vpad + ascent; + grub_video_draw_string ("Select an item with the arrow keys and " + "press Enter to boot.", + view->status_font, view->status_color, 30, texty); + texty += textheight; + grub_video_draw_string ("Press: 'c' for command line; 't' to switch to " + "non-graphical menu.", + view->status_font, view->status_color, 30, texty); +} + +static void +draw_timeout (grub_gfxmenu_view_t view) +{ + int timeout = grub_gfxmenu_model_get_timeout_ms (view->model); + if (timeout == -1) + return; + + int remaining = grub_gfxmenu_model_get_timeout_remaining_ms (view->model); + if (remaining < 0) + remaining = 0; + + int t = timeout - remaining; + + /* Set the timeout bar's frame. */ + grub_video_rect_t f; + f.width = view->screen.width * 3 / 5; + f.height = view->screen.height / 25; + f.x = view->screen.x + (view->screen.width - f.width) / 2; + f.y = view->screen.y + view->screen.height - 90; + + /* First attempt just uses filled rectangles; + TODO we should enhance with a pixmap themed progress bar component. */ + + /* Border. */ + grub_video_fill_rect (view->progress_bar_border_color, + f.x - 1, f.y - 1, + f.width + 2, f.height + 2); + + /* Bar background. */ + int barwidth = f.width * t / timeout; + grub_video_fill_rect (view->progress_bar_bg_color, + f.x + barwidth, f.y, + f.width - barwidth, f.height); + + /* Bar foreground. */ + grub_video_fill_rect (view->progress_bar_fg_color, + f.x, f.y, + barwidth, f.height); + + char *text = grub_malloc (200); + if (!text) + return; + int seconds_remaining_rounded_up = (remaining + 999) / 1000; + grub_sprintf (text, + "The highlighted entry will be booted automatically in %d s.", + seconds_remaining_rounded_up); + grub_font_t font = view->status_font; + grub_video_color_t color = view->progress_bar_border_color; + + /* Center the text. */ + int text_width = grub_font_get_string_width (font, text); + int x = f.x + (f.width - text_width) / 2; + int y = (f.y + (f.height - grub_font_get_descent (font)) / 2 + + grub_font_get_ascent (font) / 2); + grub_video_draw_string (text, font, color, x, y); + grub_free (text); +} + +static void +draw_message (grub_gfxmenu_view_t view) +{ + char *text = view->progress_message_text; + if (! text) + return; + + grub_font_t font = view->status_font; + grub_video_color_t color = view->status_color; + + /* Set the timeout bar's frame. */ + grub_video_rect_t f; + f.width = view->screen.width * 4 / 5; + f.height = 50; + f.x = view->screen.x + (view->screen.width - f.width) / 2; + f.y = view->screen.y + view->screen.height - 90 - 20 - f.height; + + /* Border. */ + grub_video_fill_rect (color, + f.x-1, f.y-1, f.width+2, f.height+2); + /* Fill. */ + grub_video_fill_rect (view->status_bg_color, + f.x, f.y, f.width, f.height); + + /* Center the text. */ + int text_width = grub_font_get_string_width (font, text); + int x = f.x + (f.width - text_width) / 2; + int y = (f.y + (f.height - grub_font_get_descent (font)) / 2 + + grub_font_get_ascent (font) / 2); + grub_video_draw_string (text, font, color, x, y); +} + + +void +grub_gfxmenu_view_draw (grub_gfxmenu_view_t view) +{ + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + draw_background (view); + draw_menu (view); + draw_title (view); + draw_status (view); + draw_timeout (view); + draw_message (view); +} + +static grub_err_t +set_graphics_mode (void) +{ + const char *doublebuf_str = grub_env_get ("doublebuffering"); + int doublebuf_flags = + (doublebuf_str && doublebuf_str[0] == 'n') + ? 0 + : GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + + const char *modestr = grub_env_get ("gfxmode"); + if (grub_video_setup_preferred_mode (modestr, doublebuf_flags, 640, 480) + != GRUB_ERR_NONE) + return grub_errno; + + return GRUB_ERR_NONE; +} + +static grub_err_t +set_text_mode (void) +{ + return grub_video_restore (); +} + +static int term_target_width; +static int term_target_height; +static struct grub_video_render_target *term_target; +static int term_initialized; +static grub_term_t term_original; +static grub_gfxmenu_view_t term_view; + +static void +repaint_terminal (int x __attribute ((unused)), + int y __attribute ((unused)), + int width __attribute ((unused)), + int height __attribute ((unused))) +{ + if (! term_view) + return; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + grub_gfxmenu_view_draw (term_view); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + int termx = term_view->screen.x + + term_view->screen.width * (10 - 7) / 10 / 2; + int termy = term_view->screen.y + + term_view->screen.height * (10 - 7) / 10 / 2; + + grub_gfxmenu_box_t term_box = term_view->terminal_box; + if (term_box) + { + term_box->set_content_size (term_box, + term_target_width, term_target_height); + + term_box->draw (term_box, + termx - term_box->get_left_pad (term_box), + termy - term_box->get_top_pad (term_box)); + } + + grub_video_blit_render_target (term_target, GRUB_VIDEO_BLIT_REPLACE, + termx, termy, + 0, 0, term_target_width, term_target_height); + grub_video_swap_buffers (); +} + +static void +init_terminal (grub_gfxmenu_view_t view) +{ + term_original = grub_term_get_current (); + + term_target_width = view->screen.width * 7 / 10; + term_target_height = view->screen.height * 7 / 10; + + grub_video_create_render_target (&term_target, + term_target_width, + term_target_height, + GRUB_VIDEO_MODE_TYPE_RGB + | GRUB_VIDEO_MODE_TYPE_ALPHA); + if (grub_errno != GRUB_ERR_NONE) + return; + + /* Note: currently there is no API for changing the gfxterm font + on the fly, so whatever font the initially loaded theme specifies + will be permanent. */ + grub_gfxterm_init_window (term_target, 0, 0, + term_target_width, term_target_height, + view->terminal_font_name, 3); + if (grub_errno != GRUB_ERR_NONE) + return; + term_initialized = 1; + + /* XXX: store static pointer to the 'view' object so the repaint callback can access it. */ + term_view = view; + grub_gfxterm_set_repaint_callback (repaint_terminal); + grub_term_set_current (grub_gfxterm_get_term ()); +} + +static void destroy_terminal (void) +{ + term_view = 0; + if (term_initialized) + grub_gfxterm_destroy_window (); + grub_gfxterm_set_repaint_callback (0); + if (term_target) + grub_video_delete_render_target (term_target); + if (term_original) + grub_term_set_current (term_original); +} + + +static void +notify_booting (void *userdata, grub_menu_entry_t entry) +{ + grub_gfxmenu_view_t view = (grub_gfxmenu_view_t) userdata; + + char *s = grub_malloc (100 + grub_strlen (entry->title)); + if (!s) + return; + + grub_sprintf (s, "Booting '%s'", entry->title); + set_progress_message (view, s); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); +} + +static void +notify_fallback (void *userdata, grub_menu_entry_t entry) +{ + grub_gfxmenu_view_t view = (grub_gfxmenu_view_t) userdata; + + char *s = grub_malloc (100 + grub_strlen (entry->title)); + if (!s) + return; + + grub_sprintf (s, "Falling back to '%s'", entry->title); + set_progress_message (view, s); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + grub_millisleep (FALLBACK_MESSAGE_DELAY); +} + +static void +notify_execution_failure (void *userdata __attribute__ ((unused))) +{ +} + + +static struct grub_menu_execute_callback execute_callback = +{ + .notify_booting = notify_booting, + .notify_fallback = notify_fallback, + .notify_failure = notify_execution_failure +}; + +int +grub_gfxmenu_view_execute_with_fallback (grub_gfxmenu_view_t view, + grub_menu_entry_t entry) +{ + grub_menu_execute_with_fallback (grub_gfxmenu_model_get_menu (view->model), + entry, &execute_callback, (void *) view); + + if (set_graphics_mode () != GRUB_ERR_NONE) + return 0; /* Failure. */ + + /* If we returned, there was a failure. */ + set_progress_message (view, grub_strdup ("Unable to automatically boot.")); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + grub_millisleep (5000); + set_progress_message (view, 0); /* Clear the message. */ + + return 1; /* Ok. */ +} + +int +grub_gfxmenu_view_execute_entry (grub_gfxmenu_view_t view, + grub_menu_entry_t entry) +{ + /* Currently we switch back to text mode by restoring + the original terminal before executing the menu entry. + It is hard to make it work when executing a menu entry + that switches video modes -- it using gfxterm in a + window, the repaint callback seems to crash GRUB. */ + /* TODO: Determine if this works when 'gfxterm' was set as + the current terminal before invoking the gfxmenu. */ + destroy_terminal (); + + grub_menu_execute_entry (entry); + if (grub_errno != GRUB_ERR_NONE) + grub_wait_after_message (); + + if (set_graphics_mode () != GRUB_ERR_NONE) + return 0; /* Failure. */ + + init_terminal (view); + return 1; /* Ok. */ +} + +void +grub_gfxmenu_view_run_terminal (grub_gfxmenu_view_t view __attribute__((unused))) +{ + grub_cmdline_run (1); +} + === added file 'gfxmenu/widget-box.c' --- gfxmenu/widget-box.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/widget-box.c 2008-07-19 18:25:18 +0000 @@ -0,0 +1,244 @@ +/* widget_box.c - Pixmap-stylized box widget. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +enum box_pixmaps +{ + BOX_PIXMAP_NW, BOX_PIXMAP_NE, BOX_PIXMAP_SE, BOX_PIXMAP_SW, + BOX_PIXMAP_N, BOX_PIXMAP_E, BOX_PIXMAP_S, BOX_PIXMAP_W, + BOX_PIXMAP_CENTER +}; + +static const char *box_pixmap_names[] = { + /* Corners: */ + "nw", "ne", "se", "sw", + /* Sides: */ + "n", "e", "s", "w", + /* Center: */ + "c" +}; + +#define BOX_NUM_PIXMAPS (sizeof(box_pixmap_names)/sizeof(*box_pixmap_names)) + +static void +draw (grub_gfxmenu_box_t self, int x, int y) +{ + int height_n; + int height_s; + int height_e; + int height_w; + int width_n; + int width_s; + int width_e; + int width_w; + unsigned i; + + /* Don't try to draw if any pixmaps are null, except the center. */ + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + if (i != BOX_PIXMAP_CENTER && self->scaled_pixmaps[i] == 0) + return; + } + + height_n = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_N]); + height_s = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_S]); + height_e = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_E]); + height_w = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_W]); + width_n = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_N]); + width_s = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_S]); + width_e = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_E]); + width_w = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_W]); + + /* Draw sides. */ + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_N], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y, 0, 0, width_n, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_S], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y + height_n + height_w, + 0, 0, width_s, height_s); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_E], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y + height_n, + 0, 0, width_e, height_e); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_W], + GRUB_VIDEO_BLIT_BLEND, + x, y + height_n, 0, 0, width_w, height_w); + + /* Draw corners. */ + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_NW], + GRUB_VIDEO_BLIT_BLEND, + x, y, 0, 0, width_w, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_NE], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y, + 0, 0, width_e, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_SE], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y + height_n + height_e, + 0, 0, width_e, height_s); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_SW], + GRUB_VIDEO_BLIT_BLEND, + x, y + height_n + height_w, + 0, 0, width_w, height_s); + + /* Draw center. */ + if (self->scaled_pixmaps[BOX_PIXMAP_CENTER]) + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_CENTER], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y + height_n, + 0, 0, self->width, self->height); +} + +static void +scale_pixmap (grub_gfxmenu_box_t self, int i, int w, int h) +{ + struct grub_video_bitmap **scaled = &self->scaled_pixmaps[i]; + struct grub_video_bitmap *raw = self->raw_pixmaps[i]; + + if (raw == 0) + return; + + if (w == -1) + w = grub_video_bitmap_get_width (raw); + if (h == -1) + h = grub_video_bitmap_get_height (raw); + + if (*scaled == 0 + || ((int) grub_video_bitmap_get_width (*scaled) != w) + || ((int) grub_video_bitmap_get_height (*scaled) != h)) + { + if (*scaled) + { + grub_video_bitmap_destroy (*scaled); + *scaled = 0; + } + grub_video_bitmap_create_scaled (scaled, w, h, raw, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + } +} + +static void +set_content_size (grub_gfxmenu_box_t self, + int width, int height) +{ + self->width = width; + self->height = height; + /* Resize sides to match the width and height. */ + /* It is assumed that the corners width/height match the adjacent sides. */ + + /* Resize N and S sides to match width. */ + scale_pixmap(self, BOX_PIXMAP_N, width, -1); + scale_pixmap(self, BOX_PIXMAP_S, width, -1); + + /* Resize E and W sides to match height. */ + scale_pixmap(self, BOX_PIXMAP_E, -1, height); + scale_pixmap(self, BOX_PIXMAP_W, -1, height); + + /* Don't scale the corners--they are assumed to match the sides. */ + scale_pixmap(self, BOX_PIXMAP_NW, -1, -1); + scale_pixmap(self, BOX_PIXMAP_SW, -1, -1); + scale_pixmap(self, BOX_PIXMAP_NE, -1, -1); + scale_pixmap(self, BOX_PIXMAP_SE, -1, -1); + + /* Scale the center area. */ + scale_pixmap(self, BOX_PIXMAP_CENTER, width, height); +} + +static int +get_left_pad (grub_gfxmenu_box_t self) +{ + return grub_video_bitmap_get_width (self->raw_pixmaps[BOX_PIXMAP_W]); +} + +static int +get_top_pad (grub_gfxmenu_box_t self) +{ + return grub_video_bitmap_get_height (self->raw_pixmaps[BOX_PIXMAP_N]); +} + +static void +destroy (grub_gfxmenu_box_t self) +{ + unsigned i; + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + if (self->raw_pixmaps[i]) + grub_video_bitmap_destroy(self->raw_pixmaps[i]); + self->raw_pixmaps[i] = 0; + + if (self->scaled_pixmaps[i]) + grub_video_bitmap_destroy(self->scaled_pixmaps[i]); + self->scaled_pixmaps[i] = 0; + } + grub_free (self->raw_pixmaps); + self->raw_pixmaps = 0; + grub_free (self->scaled_pixmaps); + self->scaled_pixmaps = 0; + + grub_free (self); /* Free self: must be the last step! */ +} + + +grub_gfxmenu_box_t +grub_gfxmenu_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix) +{ + char path[200]; + unsigned i; + grub_gfxmenu_box_t box; + + box = (grub_gfxmenu_box_t) grub_malloc (sizeof (*box)); + if (!box) + return 0; + box->width = 0; + box->height = 0; + box->raw_pixmaps = + (struct grub_video_bitmap **) + grub_malloc (BOX_NUM_PIXMAPS * sizeof (struct grub_video_bitmap *)); + box->scaled_pixmaps = + (struct grub_video_bitmap **) + grub_malloc (BOX_NUM_PIXMAPS * sizeof (struct grub_video_bitmap *)); + + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + /* TODO XXX dynamically allocate PATH, making sure it's large enough */ + grub_sprintf (path, "%s%s%s", + pixmaps_prefix, box_pixmap_names[i], pixmaps_suffix); + grub_video_bitmap_load (&box->raw_pixmaps[i], path); + box->scaled_pixmaps[i] = 0; + } + + box->draw = draw; + box->set_content_size = set_content_size; + box->get_left_pad = get_left_pad; + box->get_top_pad = get_top_pad; + box->destroy = destroy; + + return box; +} + === added file 'include/grub/gfxmenu_model.h' --- include/grub/gfxmenu_model.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxmenu_model.h 2008-07-26 23:37:04 +0000 @@ -0,0 +1,59 @@ +/* gfxmenu_model.h - gfxmenu model interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXMENU_MODEL_HEADER +#define GRUB_GFXMENU_MODEL_HEADER 1 + +#include + +struct grub_gfxmenu_model; /* Forward declaration of opaque type. */ +typedef struct grub_gfxmenu_model *grub_gfxmenu_model_t; + + +grub_gfxmenu_model_t grub_gfxmenu_model_new (grub_menu_t menu); + +void grub_gfxmenu_model_destroy (grub_gfxmenu_model_t model); + +grub_menu_t grub_gfxmenu_model_get_menu (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_set_timeout (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_clear_timeout (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_timeout_ms (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_timeout_remaining_ms (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_timeout_expired (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_num_entries (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_selected_index (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_set_selected_index (grub_gfxmenu_model_t model, + int index); + +const char *grub_gfxmenu_model_get_entry_title (grub_gfxmenu_model_t model, + int index); + +grub_menu_entry_t grub_gfxmenu_model_get_entry (grub_gfxmenu_model_t model, + int index); + +#endif /* GRUB_GFXMENU_MODEL_HEADER */ + === added file 'include/grub/gfxmenu_view.h' --- include/grub/gfxmenu_view.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxmenu_view.h 2008-07-26 23:37:04 +0000 @@ -0,0 +1,53 @@ +/* gfxmenu_view.h - gfxmenu view interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXMENU_VIEW_HEADER +#define GRUB_GFXMENU_VIEW_HEADER 1 + +#include +#include +#include +#include + +struct grub_gfxmenu_view; /* Forward declaration of opaque type. */ +typedef struct grub_gfxmenu_view *grub_gfxmenu_view_t; + + +grub_gfxmenu_view_t grub_gfxmenu_view_new (const char *theme_path, + grub_gfxmenu_model_t model); + +void grub_gfxmenu_view_destroy (grub_gfxmenu_view_t view); + +/* Set properties on the view based on settings from the specified + theme file. */ +int grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, + const char *theme_path); + +void grub_gfxmenu_view_draw (grub_gfxmenu_view_t view); + +int grub_gfxmenu_view_execute_with_fallback (grub_gfxmenu_view_t view, + grub_menu_entry_t entry); + +int grub_gfxmenu_view_execute_entry (grub_gfxmenu_view_t view, + grub_menu_entry_t entry); + +void grub_gfxmenu_view_run_terminal (grub_gfxmenu_view_t view); + +#endif /* GRUB_GFXMENU_VIEW_HEADER */ + === added file 'include/grub/gfxterm.h' --- include/grub/gfxterm.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxterm.h 2008-07-19 19:56:06 +0000 @@ -0,0 +1,41 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXTERM_HEADER +#define GRUB_GFXTERM_HEADER 1 + +#include +#include +#include +#include + +grub_err_t +grub_gfxterm_init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width); + +void grub_gfxterm_destroy_window (void); + +grub_term_t grub_gfxterm_get_term (void); + +typedef void (*grub_gfxterm_repaint_callback_t)(int x, int y, + int width, int height); + +void grub_gfxterm_set_repaint_callback (grub_gfxterm_repaint_callback_t func); + +#endif /* ! GRUB_GFXTERM_HEADER */ === added file 'include/grub/gfxwidgets.h' --- include/grub/gfxwidgets.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxwidgets.h 2008-07-19 18:25:18 +0000 @@ -0,0 +1,47 @@ +/* gfxwidgets.h - Widgets for the graphical menu (gfxmenu). */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXWIDGETS_HEADER +#define GRUB_GFXWIDGETS_HEADER 1 + +#include + +typedef struct grub_gfxmenu_box *grub_gfxmenu_box_t; + +struct grub_gfxmenu_box +{ + /* The size of the content. */ + int width; + int height; + + struct grub_video_bitmap **raw_pixmaps; + struct grub_video_bitmap **scaled_pixmaps; + + void (*draw) (grub_gfxmenu_box_t self, int x, int y); + void (*set_content_size) (grub_gfxmenu_box_t self, + int width, int height); + int (*get_left_pad) (grub_gfxmenu_box_t self); + int (*get_top_pad) (grub_gfxmenu_box_t self); + void (*destroy) (grub_gfxmenu_box_t self); +}; + +grub_gfxmenu_box_t grub_gfxmenu_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix); + +#endif /* ! GRUB_GFXWIDGETS_HEADER */ === added file 'include/grub/menu.h' --- include/grub/menu.h 1970-01-01 00:00:00 +0000 +++ include/grub/menu.h 2008-07-19 18:40:28 +0000 @@ -0,0 +1,51 @@ +/* menu.h - Menu and menu entry model declarations. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_MENU_HEADER +#define GRUB_MENU_HEADER 1 + +/* The menu entry. */ +struct grub_menu_entry +{ + /* The title name. */ + const char *title; + + /* The commands associated with this menu entry. */ + struct grub_script *commands; + + /* The sourcecode of the menu entry, used by the editor. */ + const char *sourcecode; + + /* The next element. */ + struct grub_menu_entry *next; +}; +typedef struct grub_menu_entry *grub_menu_entry_t; + +/* The menu. */ +struct grub_menu +{ + /* The size of a menu. */ + int size; + + /* The list of menu entries. */ + grub_menu_entry_t entry_list; +}; +typedef struct grub_menu *grub_menu_t; + +#endif /* GRUB_MENU_HEADER */ === added file 'include/grub/menu_viewer.h' --- include/grub/menu_viewer.h 1970-01-01 00:00:00 +0000 +++ include/grub/menu_viewer.h 2008-07-19 19:07:47 +0000 @@ -0,0 +1,48 @@ +/* menu_viewer.h - Interface to menu viewer implementations. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_MENU_VIEWER_HEADER +#define GRUB_MENU_VIEWER_HEADER 1 + +#include +#include +#include +#include + +struct grub_menu_viewer +{ + /* The menu viewer name. */ + const char *name; + + grub_err_t (*show_menu) (grub_menu_t menu, int nested); + + struct grub_menu_viewer *next; +}; +typedef struct grub_menu_viewer *grub_menu_viewer_t; + +void EXPORT_FUNC(grub_menu_viewer_init) (void); + +void EXPORT_FUNC(grub_menu_viewer_register) (grub_menu_viewer_t viewer); + +grub_err_t EXPORT_FUNC(grub_menu_viewer_show_menu) (grub_menu_t menu, int nested); + +/* Return nonzero iff the menu viewer should clean up and return ASAP. */ +int EXPORT_FUNC(grub_menu_viewer_should_return) (void); + +#endif /* GRUB_MENU_VIEWER_HEADER */ === modified file 'include/grub/normal.h' --- include/grub/normal.h 2008-03-26 12:01:02 +0000 +++ include/grub/normal.h 2008-07-26 23:37:04 +0000 @@ -25,6 +25,7 @@ #include #include #include +#include /* The maximum size of a command-line. */ #define GRUB_MAX_CMDLINE 1600 @@ -84,34 +85,6 @@ }; typedef struct grub_command *grub_command_t; -/* The menu entry. */ -struct grub_menu_entry -{ - /* The title name. */ - const char *title; - - /* The commands associated with this menu entry. */ - struct grub_script *commands; - - /* The sourcecode of the menu entry, used by the editor. */ - const char *sourcecode; - - /* The next element. */ - struct grub_menu_entry *next; -}; -typedef struct grub_menu_entry *grub_menu_entry_t; - -/* The menu. */ -struct grub_menu -{ - /* The size of a menu. */ - int size; - - /* The list of menu entries. */ - grub_menu_entry_t entry_list; -}; -typedef struct grub_menu *grub_menu_t; - /* This is used to store the names of filesystem modules for auto-loading. */ struct grub_fs_module_list { @@ -123,10 +96,27 @@ /* To exit from the normal mode. */ extern grub_jmp_buf grub_exit_env; +extern struct grub_menu_viewer grub_normal_terminal_menu_viewer; + +typedef struct grub_menu_execute_callback +{ + void (*notify_booting) (void *userdata, grub_menu_entry_t entry); + void (*notify_fallback) (void *userdata, grub_menu_entry_t entry); + void (*notify_failure) (void *userdata); +} +*grub_menu_execute_callback_t; + void grub_enter_normal_mode (const char *config); void grub_normal_execute (const char *config, int nested); -void grub_menu_run (grub_menu_t menu, int nested); +void grub_menu_execute_with_fallback (grub_menu_t menu, + grub_menu_entry_t entry, + grub_menu_execute_callback_t callback, + void *callback_data); void grub_menu_entry_run (grub_menu_entry_t entry); +int grub_menu_get_default_entry_index (grub_menu_t menu); +int grub_menu_get_timeout (void); +void grub_menu_set_timeout (int timeout); +void grub_menu_execute_entry(grub_menu_entry_t entry); void grub_cmdline_run (int nested); int grub_cmdline_get (const char *prompt, char cmdline[], unsigned max_len, int echo_char, int readline); === added file 'include/grub/stringutil.h' --- include/grub/stringutil.h 1970-01-01 00:00:00 +0000 +++ include/grub/stringutil.h 2008-07-25 15:52:57 +0000 @@ -0,0 +1,32 @@ +/* gfxmenu_stringutil.h - String utilities for the graphical menu interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXMENU_STRINGUTIL_HEADER +#define GRUB_GFXMENU_STRINGUTIL_HEADER 1 + +#include + +char *grub_new_substring (const char *buf, + grub_size_t start, grub_size_t end); + +char *grub_resolve_relative_path (const char *base, const char *path); + +char *grub_get_dirname (const char *file_path); + +#endif /* GRUB_GFXMENU_STRINGUTIL_HEADER */ === modified file 'include/grub/video.h' --- include/grub/video.h 2008-07-03 14:12:08 +0000 +++ include/grub/video.h 2008-07-19 19:31:46 +0000 @@ -151,6 +151,16 @@ grub_uint8_t a; /* Reserved bits value (0-255). */ }; +/* A 2D rectangle type. */ +struct grub_video_rect +{ + int x; + int y; + int width; + int height; +}; +typedef struct grub_video_rect grub_video_rect_t; + struct grub_video_adapter { /* The video adapter name. */ @@ -307,4 +317,21 @@ grub_err_t grub_video_get_active_render_target (struct grub_video_render_target **target); + +/* Defined in video/setmode.c */ + +/* Set the video mode based on the preferred modes specified in MODE_LIST in + the form: x[x][;...] + + For example: 640x480;800x600x8;400x300x32 + + If MODE_LIST is null, or no modes in it are usable, then DEFAULT_WIDTH and + DEFAULT_HEIGHT are used to set the mode. The MODE_FLAGS argument determines + the video mode flags such as double buffering that are used. */ + +grub_err_t +grub_video_setup_preferred_mode (const char *mode_list, int mode_flags, + int default_width, int default_height); + + #endif /* ! GRUB_VIDEO_HEADER */ === added file 'kern/menu_viewer.c' --- kern/menu_viewer.c 1970-01-01 00:00:00 +0000 +++ kern/menu_viewer.c 2008-07-19 19:07:47 +0000 @@ -0,0 +1,99 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include + +/* The list of menu viewers. */ +static grub_menu_viewer_t menu_viewer_list; + +static int should_return; +static int menu_viewer_changed; + +void +grub_menu_viewer_register (grub_menu_viewer_t viewer) +{ + viewer->next = menu_viewer_list; + menu_viewer_list = viewer; +} + +static grub_menu_viewer_t get_current_menu_viewer (void) +{ + const char *selected_name = grub_env_get ("menuviewer"); + + /* If none selected, pick the last registered one. */ + if (selected_name == 0) + return menu_viewer_list; + + grub_menu_viewer_t cur; + for (cur = menu_viewer_list; cur; cur = cur->next) + { + if (grub_strcmp (cur->name, selected_name) == 0) + return cur; + } + + /* Fall back to the first entry (or null). */ + return menu_viewer_list; +} + +grub_err_t +grub_menu_viewer_show_menu (grub_menu_t menu, int nested) +{ + grub_err_t err; + int repeat = 0; + do + { + repeat = 0; + menu_viewer_changed = 0; + grub_menu_viewer_t cur = get_current_menu_viewer (); + if (!cur) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "No menu viewer available."); + + should_return = 0; + err = cur->show_menu (menu, nested); + if (menu_viewer_changed) + repeat = 1; + } + while (repeat); + return err; +} + +int +grub_menu_viewer_should_return (void) +{ + return should_return; +} + +static char * +menuviewer_write_hook (struct grub_env_var *var __attribute__ ((unused)), + const char *val) +{ + menu_viewer_changed = 1; + should_return = 1; + return grub_strdup (val); +} + +void +grub_menu_viewer_init (void) +{ + grub_register_variable_hook ("menuviewer", 0, menuviewer_write_hook); +} + === modified file 'normal/main.c' --- normal/main.c 2008-02-02 16:48:52 +0000 +++ normal/main.c 2008-07-19 19:07:47 +0000 @@ -28,6 +28,7 @@ #include #include #include +#include grub_jmp_buf grub_exit_env; @@ -476,7 +477,7 @@ if (menu && menu->size) { - grub_menu_run (menu, nested); + grub_menu_viewer_show_menu (menu, nested); if (nested) free_menu (menu); } @@ -519,6 +520,8 @@ if (mod) grub_dl_ref (mod); + grub_menu_viewer_register (&grub_normal_terminal_menu_viewer); + grub_set_history (GRUB_DEFAULT_HISTORY_SIZE); /* Register a command "normal" for the rescue mode. */ @@ -535,6 +538,8 @@ /* This registers some built-in commands. */ grub_command_init (); + + grub_menu_viewer_init (); } GRUB_MOD_FINI(normal) === modified file 'normal/menu.c' --- normal/menu.c 2008-02-09 11:00:19 +0000 +++ normal/menu.c 2008-07-26 23:37:04 +0000 @@ -21,9 +21,11 @@ #include #include #include +#include #include #include #include +#include static grub_uint8_t grub_color_menu_normal; static grub_uint8_t grub_color_menu_highlight; @@ -241,8 +243,8 @@ /* Return the current timeout. If the variable "timeout" is not set or invalid, return -1. */ -static int -get_timeout (void) +int +grub_menu_get_timeout (void) { char *val; int timeout; @@ -269,8 +271,8 @@ } /* Set current timeout in the variable "timeout". */ -static void -set_timeout (int timeout) +void +grub_menu_set_timeout (int timeout) { /* Ignore TIMEOUT if it is zero, because it will be unset really soon. */ if (timeout > 0) @@ -308,6 +310,57 @@ return entry; } +/* Get the default menu entry index. */ +int +grub_menu_get_default_entry_index (grub_menu_t menu) +{ + int i = get_entry_number ("default"); + + /* If DEFAULT_ENTRY is not within the menu entries, fall back to + the first entry. */ + if (i < 0 || i >= menu->size) + i = 0; + + return i; +} + +/* Get the first entry number from the variable NAME, which is a + space-separated list of nonnegative integers. The entry number which + is returned is stripped from the value of NAME. */ +static int +get_and_remove_first_entry_number (const char *name) +{ + char *val; + char *tail; + int entry; + + val = grub_env_get (name); + if (! val) + return -1; + + grub_error_push (); + + entry = (int) grub_strtoul (val, &tail, 0); + + if (grub_errno == GRUB_ERR_NONE) + { + /* Skip whitespace to find the next digit. */ + while (*tail && grub_isspace (*tail)) + tail++; + grub_env_set (name, tail); + } + else + { + grub_env_unset (name); + grub_errno = GRUB_ERR_NONE; + entry = -1; + } + + grub_error_pop (); + + return entry; +} + static void print_timeout (int timeout, int offset, int second_stage) { @@ -332,15 +385,10 @@ first = 0; - default_entry = get_entry_number ("default"); - - /* If DEFAULT_ENTRY is not within the menu entries, fall back to - the first entry. */ - if (default_entry < 0 || default_entry >= menu->size) - default_entry = 0; + default_entry = grub_menu_get_default_entry_index (menu); /* If timeout is 0, drawing is pointless (and ugly). */ - if (get_timeout () == 0) + if (grub_menu_get_timeout () == 0) return default_entry; offset = default_entry; @@ -359,15 +407,15 @@ print_entries (menu, first, offset); grub_refresh (); - timeout = get_timeout (); + timeout = grub_menu_get_timeout (); if (timeout > 0) print_timeout (timeout, offset, 0); - while (1) + while (!grub_menu_viewer_should_return ()) { int c; - timeout = get_timeout (); + timeout = grub_menu_get_timeout (); if (timeout > 0) { @@ -377,7 +425,7 @@ if (current_time - saved_time >= GRUB_TICKS_PER_SECOND) { timeout--; - set_timeout (timeout); + grub_menu_set_timeout (timeout); saved_time = current_time; print_timeout (timeout, offset, 1); } @@ -468,6 +516,10 @@ } goto refresh; + case 't': + grub_env_set ("menuviewer", "protomenu"); + goto refresh; + default: break; } @@ -476,13 +528,14 @@ } } - /* Never reach here. */ + /* Exit menu without activating an item. This occurs if the user presses + * 't', switching to the graphical menu viewer. */ return -1; } /* Run a menu entry. */ -static void -run_menu_entry (grub_menu_entry_t entry) +void +grub_menu_execute_entry(grub_menu_entry_t entry) { grub_script_execute (entry->commands); @@ -491,15 +544,80 @@ grub_command_execute ("boot", 0); } +/* Execute ENTRY from the menu MENU, falling back to entries specified + in the environment variable "fallback" if it fails. CALLBACK is a + pointer to a struct of function pointers which are used to allow the + caller provide feedback to the user. */ void -grub_menu_run (grub_menu_t menu, int nested) +grub_menu_execute_with_fallback (grub_menu_t menu, + grub_menu_entry_t entry, + grub_menu_execute_callback_t callback, + void *callback_data) +{ + int fallback_entry; + + callback->notify_booting (callback_data, entry); + + grub_menu_execute_entry (entry); + + /* Deal with fallback entries. */ + while ((fallback_entry = get_and_remove_first_entry_number ("fallback")) + >= 0) + { + grub_print_error (); + grub_errno = GRUB_ERR_NONE; + + entry = get_entry (menu, fallback_entry); + callback->notify_fallback (callback_data, entry); + grub_menu_execute_entry (entry); + } + + if (grub_errno != GRUB_ERR_NONE) + callback->notify_failure (callback_data); +} + +static void +notify_booting (void *userdata __attribute__((unused)), + grub_menu_entry_t entry) +{ + grub_printf (" Booting \'%s\'\n\n", entry->title); +} + +static void +notify_fallback (void *userdata __attribute__((unused)), + grub_menu_entry_t entry) +{ + grub_printf ("\n Falling back to \'%s\'\n\n", entry->title); + grub_millisleep (2000); +} + +static void +notify_execution_failure (void *userdata __attribute__((unused))) +{ + if (grub_errno != GRUB_ERR_NONE) + { + grub_print_error (); + grub_errno = GRUB_ERR_NONE; + + grub_wait_after_message (); + } +} + +static struct grub_menu_execute_callback execution_callback = +{ + .notify_booting = notify_booting, + .notify_fallback = notify_fallback, + .notify_failure = notify_execution_failure +}; + +static grub_err_t +show_menu (grub_menu_t menu, int nested) { while (1) { int boot_entry; grub_menu_entry_t e; - int fallback_entry; - + boot_entry = run_menu (menu, nested); if (boot_entry < 0) break; @@ -507,34 +625,18 @@ e = get_entry (menu, boot_entry); if (! e) continue; /* Menu is empty. */ - + grub_cls (); grub_setcursor (1); - grub_printf (" Booting \'%s\'\n\n", e->title); - - run_menu_entry (e); - - /* Deal with a fallback entry. */ - /* FIXME: Multiple fallback entries like GRUB Legacy. */ - fallback_entry = get_entry_number ("fallback"); - if (fallback_entry >= 0) - { - grub_print_error (); - grub_errno = GRUB_ERR_NONE; - - e = get_entry (menu, fallback_entry); - grub_env_unset ("fallback"); - grub_printf ("\n Falling back to \'%s\'\n\n", e->title); - run_menu_entry (e); - } - - if (grub_errno != GRUB_ERR_NONE) - { - grub_print_error (); - grub_errno = GRUB_ERR_NONE; - - grub_wait_after_message (); - } + grub_menu_execute_with_fallback (menu, e, &execution_callback, 0); } + + return GRUB_ERR_NONE; } + +struct grub_menu_viewer grub_normal_terminal_menu_viewer = +{ + .name = "terminal", + .show_menu = show_menu +}; === modified file 'term/gfxterm.c' --- term/gfxterm.c 2008-07-03 14:28:01 +0000 +++ term/gfxterm.c 2008-07-19 19:56:06 +0000 @@ -28,12 +28,12 @@ #include #include #include +#include #include #include #define DEFAULT_VIDEO_WIDTH 640 #define DEFAULT_VIDEO_HEIGHT 480 -#define DEFAULT_VIDEO_FLAGS 0 #define DEFAULT_BORDER_WIDTH 10 @@ -108,10 +108,20 @@ struct grub_colored_char *text_buffer; }; +static int refcount; +static struct grub_video_render_target *render_target; +static grub_video_rect_t window; static struct grub_virtual_screen virtual_screen; +static grub_gfxterm_repaint_callback_t repaint_callback; + +static grub_err_t init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width); + +static void destroy_window (void); + static grub_dl_t my_mod; -static struct grub_video_mode_info mode_info; static struct grub_video_render_target *text_layer; @@ -235,287 +245,129 @@ } static grub_err_t +init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width) +{ + /* Clean up any prior instance. */ + destroy_window (); + + /* Create virtual screen. */ + if (grub_virtual_screen_setup (border_width, border_width, + width - 2 * border_width, + height - 2 * border_width, + font_name) + != GRUB_ERR_NONE) + { + return grub_errno; + } + + /* Set the render target. */ + render_target = target; + + /* Set window bounds. */ + window.x = x; + window.y = y; + window.width = width; + window.height = height; + + /* Mark whole window as dirty. */ + dirty_region_reset (); + dirty_region_add (0, 0, width, height); + + return (grub_errno = GRUB_ERR_NONE); +} + +grub_err_t +grub_gfxterm_init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width) +{ + grub_errno = GRUB_ERR_NONE; + if (refcount++ == 0) + init_window (target, x, y, width, height, font_name, border_width); + return grub_errno; +} + +static grub_err_t grub_gfxterm_init (void) { - char *font_name; - char *modevar; - int width = DEFAULT_VIDEO_WIDTH; - int height = DEFAULT_VIDEO_HEIGHT; - int depth = -1; - int flags = DEFAULT_VIDEO_FLAGS; - grub_video_color_t color; - - /* Select the font to use. */ - font_name = grub_env_get ("gfxterm_font"); - if (!font_name) - font_name = ""; /* Allow fallback to any font. */ + /* If gfxterm has already been initialized by calling the init_window + function, then leave it alone when it is set as the current terminal. */ + if (refcount++ != 0) + return GRUB_ERR_NONE; /* Parse gfxmode environment variable if set. */ - modevar = grub_env_get ("gfxmode"); - if (modevar) - { - char *tmp; - char *next_mode; - char *current_mode; - char *param; - char *value; - int mode_found = 0; - - /* Take copy of env.var. as we don't want to modify that. */ - tmp = grub_strdup (modevar); - modevar = tmp; - - if (grub_errno != GRUB_ERR_NONE) - return grub_errno; - - /* Initialize next mode. */ - next_mode = modevar; - - /* Loop until all modes has been tested out. */ - while (next_mode != NULL) - { - /* Use last next_mode as current mode. */ - tmp = next_mode; - - /* Reset video mode settings. */ - width = DEFAULT_VIDEO_WIDTH; - height = DEFAULT_VIDEO_HEIGHT; - depth = -1; - flags = DEFAULT_VIDEO_FLAGS; - - /* Save position of next mode and separate modes. */ - next_mode = grub_strchr(next_mode, ';'); - if (next_mode) - { - *next_mode = 0; - next_mode++; - } - - /* Skip whitespace. */ - while (grub_isspace (*tmp)) - tmp++; - - /* Initialize token holders. */ - current_mode = tmp; - param = tmp; - value = NULL; - - /* Parse x[x]*/ - - /* Find width value. */ - value = param; - param = grub_strchr(param, 'x'); - if (param == NULL) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - *param = 0; - param++; - - width = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - /* Find height value. */ - value = param; - param = grub_strchr(param, 'x'); - if (param == NULL) - { - height = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - } - else - { - /* We have optional color depth value. */ - *param = 0; - param++; - - height = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - /* Convert color depth value. */ - value = param; - depth = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - } - - /* Try out video mode. */ - - /* If we have 8 or less bits, then assume that it is indexed color mode. */ - if ((depth <= 8) && (depth != -1)) - flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; - - /* We have more than 8 bits, then assume that it is RGB color mode. */ - if (depth > 8) - flags |= GRUB_VIDEO_MODE_TYPE_RGB; - - /* If user requested specific depth, forward that information to driver. */ - if (depth != -1) - flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) - & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; - - /* Try to initialize requested mode. Ignore any errors. */ - grub_error_push (); - if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) - { - grub_error_pop (); - continue; - } - - /* Figure out what mode we ended up. */ - if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) - { - /* Couldn't get video mode info, restore old mode and continue to next one. */ - grub_error_pop (); - - grub_video_restore (); - continue; - } - - /* Restore state of error stack. */ - grub_error_pop (); - - /* Mode found! Exit loop. */ - mode_found = 1; - break; - } - - /* Free memory. */ - grub_free (modevar); - - if (!mode_found) - return grub_error (GRUB_ERR_BAD_ARGUMENT, - "No suitable mode found."); - } - else - { - /* No gfxmode variable set, use defaults. */ - - /* If we have 8 or less bits, then assume that it is indexed color mode. */ - if ((depth <= 8) && (depth != -1)) - flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; - - /* We have more than 8 bits, then assume that it is RGB color mode. */ - if (depth > 8) - flags |= GRUB_VIDEO_MODE_TYPE_RGB; - - /* If user requested specific depth, forward that information to driver. */ - if (depth != -1) - flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) - & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; - - /* Initialize user requested mode. */ - if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) - return grub_errno; - - /* Figure out what mode we ended up. */ - if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) - { - grub_video_restore (); - return grub_errno; - } + const char *modevar = grub_env_get ("gfxmode"); + if (grub_video_setup_preferred_mode (modevar, 0, + DEFAULT_VIDEO_WIDTH, + DEFAULT_VIDEO_HEIGHT) + != GRUB_ERR_NONE) + return grub_errno; + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + { + grub_video_restore (); + return grub_errno; } /* Make sure screen is black. */ - color = grub_video_map_rgb (0, 0, 0); - grub_video_fill_rect (color, 0, 0, mode_info.width, mode_info.height); + grub_video_fill_rect (grub_video_map_rgb (0, 0, 0), + 0, 0, mode_info.width, mode_info.height); bitmap = 0; + /* Select the font to use. */ + char *font_name = grub_env_get ("gfxterm_font"); + if (!font_name) + font_name = ""; /* Allow fallback to any font. */ + /* Leave borders for virtual screen. */ - width = mode_info.width - (2 * DEFAULT_BORDER_WIDTH); - height = mode_info.height - (2 * DEFAULT_BORDER_WIDTH); - - /* Create virtual screen. */ - if (grub_virtual_screen_setup (DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_WIDTH, - width, height, font_name) != GRUB_ERR_NONE) + if (init_window (GRUB_VIDEO_RENDER_TARGET_DISPLAY, + 0, 0, mode_info.width, mode_info.height, + font_name, + DEFAULT_BORDER_WIDTH) != GRUB_ERR_NONE) { grub_video_restore (); return grub_errno; } - /* Mark whole screen as dirty. */ - dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); - return (grub_errno = GRUB_ERR_NONE); } +static void +destroy_window (void) +{ + if (bitmap) + { + grub_video_bitmap_destroy (bitmap); + bitmap = 0; + } + + repaint_callback = 0; + grub_virtual_screen_free (); +} + +void +grub_gfxterm_destroy_window (void) +{ + if (--refcount == 0) + destroy_window (); +} + static grub_err_t grub_gfxterm_fini (void) { - if (bitmap) + /* Don't destroy an explicitly initialized terminal instance when it is + unset as the current terminal. */ + if (--refcount == 0) { - grub_video_bitmap_destroy (bitmap); - bitmap = 0; + destroy_window (); + grub_video_restore (); } - grub_virtual_screen_free (); - - grub_video_restore (); - - return GRUB_ERR_NONE; + return (grub_errno = GRUB_ERR_NONE); } static void @@ -523,9 +375,15 @@ unsigned int width, unsigned int height) { grub_video_color_t color; - - grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); - + grub_video_rect_t saved_view; + + grub_video_set_active_render_target (render_target); + /* Save viewport and set it to our window. */ + grub_video_get_viewport ((unsigned *) &saved_view.x, + (unsigned *) &saved_view.y, + (unsigned *) &saved_view.width, + (unsigned *) &saved_view.height); + grub_video_set_viewport (window.x, window.y, window.width, window.height); if (bitmap) { @@ -592,6 +450,14 @@ y - virtual_screen.offset_y, width, height); } + + /* Restore saved viewport. */ + grub_video_set_viewport (saved_view.x, saved_view.y, + saved_view.width, saved_view.height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + if (repaint_callback) + repaint_callback (x, y, width, height); } static void @@ -786,7 +652,16 @@ dirty_region_add_virtualscreen (); } else - { + { + grub_video_rect_t saved_view; + grub_video_set_active_render_target (render_target); + /* Save viewport and set it to our window. */ + grub_video_get_viewport ((unsigned *) &saved_view.x, + (unsigned *) &saved_view.y, + (unsigned *) &saved_view.width, + (unsigned *) &saved_view.height); + grub_video_set_viewport (window.x, window.y, window.width, window.height); + /* Clear new border area. */ grub_video_fill_rect (color, virtual_screen.offset_x, virtual_screen.offset_y, @@ -795,10 +670,18 @@ /* Scroll physical screen. */ grub_video_scroll (color, 0, -virtual_screen.char_height); + /* Restore saved viewport. */ + grub_video_set_viewport (saved_view.x, saved_view.y, + saved_view.width, saved_view.height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + /* Draw cursor if visible. */ if (virtual_screen.cursor_state) write_cursor (); } + + if (repaint_callback) + repaint_callback (window.x, window.y, window.width, window.height); } static void @@ -898,7 +781,7 @@ } static grub_ssize_t -grub_gfxterm_getcharwidth (grub_uint32_t c) +grub_gfxterm_getcharwidth (grub_uint32_t c __attribute__((unused))) { #if 0 struct grub_font_glyph *glyph; @@ -970,7 +853,8 @@ /* Clear text layer. */ grub_video_set_active_render_target (text_layer); color = virtual_screen.bg_color; - grub_video_fill_rect (color, 0, 0, mode_info.width, mode_info.height); + grub_video_fill_rect (color, 0, 0, + virtual_screen.width, virtual_screen.height); grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); /* Mark virtual screen to be redrawn. */ @@ -1039,6 +923,11 @@ dirty_region_redraw (); } +void +grub_gfxterm_set_repaint_callback (grub_gfxterm_repaint_callback_t func) +{ + repaint_callback = func; +} /* Option array indices. */ #define BACKGROUND_CMD_ARGINDEX_MODE 0 @@ -1066,7 +955,7 @@ /* Mark whole screen as dirty. */ dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); + dirty_region_add (0, 0, window.width, window.height); } /* If filename was provided, try to load that. */ @@ -1082,13 +971,13 @@ || grub_strcmp (state[BACKGROUND_CMD_ARGINDEX_MODE].arg, "stretch") == 0) { - if (mode_info.width != grub_video_bitmap_get_width (bitmap) - || mode_info.height != grub_video_bitmap_get_height (bitmap)) + if (window.width != (int) grub_video_bitmap_get_width (bitmap) + || window.height != (int) grub_video_bitmap_get_height (bitmap)) { struct grub_video_bitmap *scaled_bitmap; grub_video_bitmap_create_scaled (&scaled_bitmap, - mode_info.width, - mode_info.height, + window.width, + window.height, bitmap, GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); if (grub_errno == GRUB_ERR_NONE) @@ -1110,7 +999,7 @@ /* Mark whole screen as dirty. */ dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); + dirty_region_add (0, 0, window.width, window.height); } } @@ -1141,9 +1030,16 @@ .next = 0 }; +grub_term_t +grub_gfxterm_get_term (void) +{ + return &grub_video_term; +} + GRUB_MOD_INIT(term_gfxterm) { my_mod = mod; + refcount = 0; grub_term_register (&grub_video_term); grub_register_command ("background_image", === modified file 'video/i386/pc/vbe.c' --- video/i386/pc/vbe.c 2008-07-09 20:59:34 +0000 +++ video/i386/pc/vbe.c 2008-07-19 19:42:37 +0000 @@ -652,6 +652,8 @@ if (doublebuf_pageflipping_commit () != GRUB_ERR_NONE) return 1; /* Unable to set the display start. */ + framebuffer.render_target.mode_info.mode_type + |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; return 0; } @@ -690,6 +692,8 @@ doublebuf_state.update_screen = doublebuf_blit_update_screen; doublebuf_state.destroy = doublebuf_blit_destroy; + framebuffer.render_target.mode_info.mode_type + |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; return 0; } @@ -713,6 +717,9 @@ framebuffer.render_target.data = framebuffer.ptr; doublebuf_state.update_screen = doublebuf_null_update_screen; doublebuf_state.destroy = doublebuf_null_destroy; + + framebuffer.render_target.mode_info.mode_type + &= ~GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; return 0; } === added file 'video/setmode.c' --- video/setmode.c 1970-01-01 00:00:00 +0000 +++ video/setmode.c 2008-07-19 19:31:46 +0000 @@ -0,0 +1,249 @@ +/* video/setmode.c - Smart video mode selection based on preferences. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +/* Set the video mode based on the preferred modes specified in MODE_LIST in + the form: x[x][;...] + + For example: 640x480;800x600x8;400x300x32 + + If MODE_LIST is null, or no modes in it are usable, then DEFAULT_WIDTH and + DEFAULT_HEIGHT are used to set the mode. The MODE_FLAGS argument determines + the video mode flags such as double buffering that are used. */ + +grub_err_t +grub_video_setup_preferred_mode (const char *mode_list, int mode_flags, + int default_width, int default_height) +{ + int mode_found = 0; + + if (mode_list != NULL) + { + /* Take copy of mode_list as we don't want tat. */ + char *const modes_copy = grub_strdup (mode_list); + if (modes_copy == NULL) + return grub_errno; + + /* Initialize next mode. */ + char *next_mode = modes_copy; + + /* Loop until all modes has been tested out. */ + while ((next_mode != NULL) && !mode_found) + { + /* Use last next_mode as current mode. */ + char *tmp = next_mode; + + int width = -1; + int height = -1; + int depth = -1; + + /* Save position of next mode and separate modes. */ + next_mode = grub_strchr(next_mode, ';'); + if (next_mode) + { + *next_mode = 0; + next_mode++; + } + + /* Skip whitespace. */ + while (grub_isspace (*tmp)) + tmp++; + + /* Initialize token holders. */ + char *current_mode = tmp; + char *param = tmp; + char *value = NULL; + + /* Parse x[x]*/ + + /* Find width value. */ + value = param; + param = grub_strchr(param, 'x'); + if (param == NULL) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + *param = 0; + param++; + + width = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + /* Find height value. */ + value = param; + param = grub_strchr(param, 'x'); + if (param == NULL) + { + height = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + } + else + { + /* We have optional color depth value. */ + *param = 0; + param++; + + height = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + /* Convert color depth value. */ + value = param; + depth = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + } + + /* Try out video mode. */ + + int flags = mode_flags; + /* If we have <= 8 bits, assume it is an indexed color mode. */ + if ((depth <= 8) && (depth != -1)) + flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; + + /* We have > 8 bits; assume that it is RGB color mode. */ + if (depth > 8) + flags |= GRUB_VIDEO_MODE_TYPE_RGB; + + /* If user requested specific depth, pass the request to driver. */ + if (depth != -1) + flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) + & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; + + /* Try to initialize requested mode. Ignore any errors. */ + grub_error_push (); + if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) + { + grub_error_pop (); + continue; + } + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + { + /* Couldn't get video mode info, restore old mode + and continue to next one. */ + grub_error_pop (); + + grub_video_restore (); + continue; + } + + /* Restore state of error stack. */ + grub_error_pop (); + + /* Mode found! Exit loop. */ + mode_found = 1; + } + + /* Free memory. */ + grub_free (modes_copy); + } + + if (!mode_found) + { + /* No gfxmode variable set, or no listed mode was supported. + Use the caller-specified defaults. */ + int flags = mode_flags | GRUB_VIDEO_MODE_TYPE_RGB; + + /* Initialize user requested mode. */ + if (grub_video_setup (default_width, default_height, flags) + != GRUB_ERR_NONE) + return grub_errno; + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + grub_video_restore (); + else + mode_found = 1; + } + + if (!mode_found) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "No suitable mode found."); + + return (grub_errno = GRUB_ERR_NONE); +} # Begin bundle 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 lw/LVmjKjKmuoff8OxPNc0HKM6pNNeBr7IATOxQ/O4GBoLtiY1blfLERMfWL29rJoeXtKp5yPDr7 Sp5U7J9mygPkahW9vQ6b6BBriOgcYsnJlFHZuqO7s6O6Mj6UYROh3QvA9m6fZe6CqgoqRPToiJnX Z7RMvsWmXxETsc7sDBLK7AhyUNLJAkIMIVxeMXtuL2GOOf0MecH8rL7/Bzn+SgwIRT9UkQkEkD9k BJiBCSBKA4jDxz/F7/4H93fz3N/ck99Zn786Ss35xyytWWGm5yZtXiYDZFK1yMmlYrCvK2v66/F7 34v7b+39azb1+LTscfRrr/BRcPl0b8KM0fn0ht1wfN48r8ffh274/r5vUiHu2ohF8O/cgdpynSl2 +IWWrGBDnC7TerEoS1OzPJTEQugOv6fX69B216atajTOopDcqHLdev4qRXBzKbs1Y6jMuU0RqodK xZrSPp9/4n3NZ7ldPc7PxvqccfW/WeXve95aVOjJsqdFT6j8pY8FTZo6PY+v5efafR8J+KIaX5Z7 dKe3ikfMtza3Ctj4zhg1Go882JPbCHsjTrpOMKTV5uOH8p674muZ+SsZ+fPrz9nrr0S/r8nLO0D1 zrt4vxzdzWPPHsvNXgyse4aeKpYzjV08LtuK6zll2/V7LL6eO665tjq3d2F+nZ3dbQ27Lrl3at3V Zo9cm5t3HW3dZYShGO849kSTQdpQk03pB4wpH6D4mbez20oPovLOM2MTqfUHROHWrw73iv6IyK+g 5fyoOyzvZ6zBxTQUed/Y7BdCD+8/fBoPnZFSjNhmS4FyNEk3qZimg4Ooqxt8iIeLDnA9UP+tUVEo DBRWEBVkREEnjdedv0390Gxp5elu/w9MfQ5jwuhB290PVfG2unu92G/pogh40Q6ehEIdP/JqiRD/ 6P+r4bv/HAKn/xRUZzIEqQCSxS0Bv+wwpn9GSEC5LIKUQpRAoxkiRCn6ChWUyYCGFpKSwoFAlE/l h0yBZIMZIEUIPuF5hPGK70VfmbgdgvSB8r6xwQ8JcLGkIefYFk/Shtih/ws/qJCAfGB/wifm5VKZ OREKQoUIMAkGBAj83mLGy1G/504k3y+THb+WY8/2qTf8HhRiGR9yHz8fvYUY/JoeAofyCxOb/T+X ev7n6KdtKUn2Z53p+f3H9bD9pOy/9P9A/CHbswOqJ4qgWnzpEeggB54IdZxY7UuD+6IIPpivjkkh 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 4UJDpT4ISA==