# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080803040022-bxhp6libdkx40muv # target_branch: http://grub.gibibit.com/bzr/trunk-clean # testament_sha1: a9261a62ae1b371e0a9b27a0f1f55c4c880aaf1e # timestamp: 2008-08-03 07:26:21 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080803035922-trvxiqbrfge9pilr # # 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-08-03 03:57:46 +0000 +++ conf/common.rmk 2008-08-03 04:00:22 +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 16:26:46 +0000 +++ conf/i386-pc.rmk 2008-07-28 17:02:56 +0000 @@ -48,11 +48,13 @@ kern/generic/rtc_get_time_ms.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) @@ -250,7 +252,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-08-03 02:14:40 +0000 @@ -0,0 +1,1096 @@ +/* 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 + +typedef struct grub_gfxmenu_view_icon +{ + const char *class_name; + struct grub_video_bitmap *bitmap; + struct grub_gfxmenu_view_icon *next; +} *grub_gfxmenu_view_icon_t; + +/* 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; + char *theme_path; + /* Icon cache: linked list w/ dummy head node. */ + struct grub_gfxmenu_view_icon icon_cache; + + 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->theme_path = 0; + view->icon_cache.class_name = 0; + view->icon_cache.bitmap = 0; + view->icon_cache.next = 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; +} + +static void +free_icon_cache (grub_gfxmenu_view_t view) +{ + grub_gfxmenu_view_icon_t cur; + grub_gfxmenu_view_icon_t next; + for (cur = view->icon_cache.next; cur; cur = next) + { + next = cur->next; + grub_free (cur); + } + view->icon_cache.next = 0; +} + +/* 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->theme_path); + free_icon_cache (view); + 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 (view->theme_path); + view->theme_path = grub_strdup (theme_path); + free_icon_cache (view); /* There may be new icons for this theme. */ + 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 const char icon_extension[] = ".png"; + +static struct grub_video_bitmap * +try_loading_icon (grub_gfxmenu_view_t view, + const char *dir, const char *class_name) +{ + char *path = grub_malloc (grub_strlen (dir) + + grub_strlen (class_name) + + grub_strlen (icon_extension) + + 100); + if (! path) + return 0; + + grub_strcpy (path, dir); + grub_strcat (path, class_name); + grub_strcat (path, icon_extension); + + struct grub_video_bitmap *raw_bitmap; + grub_video_bitmap_load (&raw_bitmap, path); + grub_free (path); + grub_errno = GRUB_ERR_NONE; /* Critical to clear the error!! */ + if (! raw_bitmap) + return 0; + + struct grub_video_bitmap *scaled_bitmap; + grub_video_bitmap_create_scaled (&scaled_bitmap, + view->icon_width, view->icon_height, + raw_bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + grub_video_bitmap_destroy (raw_bitmap); + if (! scaled_bitmap) + return 0; + + return scaled_bitmap; +} + +static struct grub_video_bitmap * +get_icon_by_class (grub_gfxmenu_view_t view, const char *class_name) +{ + + /* First check the view's icon cache. */ + grub_gfxmenu_view_icon_t vicon; + for (vicon = view->icon_cache.next; vicon; vicon = vicon->next) + { + if (grub_strcmp (vicon->class_name, class_name) == 0) + return vicon->bitmap; + } + + /* Otherwise, we search for an icon to load. */ + if (! view->theme_path) + return 0; + + char *theme_dir = grub_get_dirname (view->theme_path); + char *icons_dir; + struct grub_video_bitmap *icon; + icon = 0; + /* First try the theme's own icons, from "grub/themes/NAME/icons/" */ + icons_dir = grub_resolve_relative_path (theme_dir, "icons/"); + if (icons_dir) + { + icon = try_loading_icon (view, icons_dir, class_name); + grub_free (icons_dir); + } + if (! icon) + { + /* If the theme doesn't have an appropriate icon, check in + "grub/themes/icons". */ + icons_dir = grub_resolve_relative_path (theme_dir, "../icons/"); + if (icons_dir) + { + icon = try_loading_icon (view, icons_dir, class_name); + grub_free (icons_dir); + } + } + grub_free (theme_dir); + + if (! icon) + return 0; + + vicon = grub_malloc (sizeof (*vicon)); + if (! vicon) + { + grub_video_bitmap_destroy (icon); + return 0; + } + vicon->class_name = class_name; + vicon->bitmap = icon; + vicon->next = view->icon_cache.next; + view->icon_cache.next = vicon; /* Link it into the cache. */ + return vicon->bitmap; +} + +static struct grub_video_bitmap * +get_item_icon (grub_gfxmenu_view_t view, int item_index) +{ + grub_menu_entry_t entry; + entry = grub_gfxmenu_model_get_entry (view->model, item_index); + if (! entry) + return 0; + + struct grub_menu_entry_class *c; + struct grub_video_bitmap *icon; + icon = 0; + for (c = entry->classes->next; c && ! icon; c = c->next) + icon = get_icon_by_class (view, c->name); + return icon; +} + +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. " + "Press SPACE to continue.")); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + while (GRUB_TERM_ASCII_CHAR(grub_getkey ()) != ' ') + { + /* Wait for SPACE to be pressed. */ + } + + 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-08-03 02:14:40 +0000 @@ -0,0 +1,63 @@ +/* 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 + +struct grub_menu_entry_class +{ + char *name; + struct grub_menu_entry_class *next; +}; + +/* The menu entry. */ +struct grub_menu_entry +{ + /* The title name. */ + const char *title; + + /* The classes associated with the menu entry: + used to choose an icon or other style attributes. + This is a dummy head node for the linked list, so for an entry E, + E.classes->next is the first class if it is not NULL. */ + struct grub_menu_entry_class *classes; + + /* 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-08-03 03:47:20 +0000 +++ include/grub/normal.h 2008-08-03 04:00:22 +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-07-29 14:07:47 +0000 +++ normal/main.c 2008-08-03 04:00:22 +0000 @@ -28,6 +28,7 @@ #include #include #include +#include grub_jmp_buf grub_exit_env; @@ -147,14 +148,132 @@ grub_env_unset_data_slot ("menu"); } +static void +free_menu_entry_classes (struct grub_menu_entry_class *head) +{ + /* Free all the classes. */ + while (head) + { + struct grub_menu_entry_class *next; + + grub_free (head->name); + next = head->next; + grub_free (head); + head = next; + } +} + +/* The tag that can be added to a menu entry's title to specify a class + for the UI to use in selecting an icon or other visual attributes. */ +static const char entry_class_attr_tag[] = "|class="; + +#define ENTRY_ATTR_SEPARATOR_CHAR ',' + +/* Parse and strip a possible "class" attribute in the title. + This code is not designed to support other attributes than "class" + in the title since, we are planning to use a better method of + specifying this information in the future. The parameter TITLE is + modified by storing a '\0' at the appropriate location to strip the + class information, if it exists. The class list is stored into *HEAD. */ +static struct grub_menu_entry_class * +get_classes_from_entry_title (char *title) +{ + struct grub_menu_entry_class *head; + char *attr_start; + + head = grub_malloc (sizeof (struct grub_menu_entry_class)); + if (! head) + return 0; + head->name = 0; + head->next = 0; + + attr_start = grub_strstr (title, entry_class_attr_tag); + if (attr_start) + { + struct grub_menu_entry_class *tail; + const char *p; + + /* Trim the properties off of the title. */ + *attr_start = '\0'; + + /* Move the pointer to the beginning of the first class name. */ + attr_start += grub_strlen (entry_class_attr_tag); + + tail = head; + p = attr_start; + while (p && *p) + { + const char *q; + const char *end; + const char *next_start; + + /* Skip any leading whitespace. */ + while (*p && grub_isspace (*p)) + p++; + + /* Find the comma terminating this one ... */ + q = grub_strchr (p, ENTRY_ATTR_SEPARATOR_CHAR); + /* ... or if it's the last one, find the '\0' terminator. */ + if (q) + { + end = q - 1; + next_start = q + 1; + } + else + { + /* For the last class, extend it to the end. */ + end = p + grub_strlen (p); + next_start = 0; + } + + /* Trim any trailing whitespace. */ + while (end > p && grub_isspace (*end)) + end--; + + grub_size_t len = end - p + 1; + /* Copy the class name into a new string. */ + char *class_name = grub_malloc (len + 1); + if (! class_name) + { + free_menu_entry_classes (head); + return 0; + } + grub_memcpy (class_name, p, len); + class_name[len] = '\0'; + + /* Create a new class and add it at the tail of the list. */ + struct grub_menu_entry_class *new_class; + new_class = grub_malloc (sizeof (struct grub_menu_entry_class)); + if (! new_class) + { + grub_free (class_name); + free_menu_entry_classes (head); + return 0; + } + /* Fill in the new class node. */ + new_class->name = class_name; + new_class->next = 0; + /* Link the tail to it, and make it the new tail. */ + tail->next = new_class; + tail = new_class; + + /* Advance the character pointer. */ + p = next_start; + } + } + + return head; +} + grub_err_t grub_normal_menu_addentry (const char *title, struct grub_script *script, const char *sourcecode) { - const char *menutitle; + char *menutitle; const char *menusourcecode; grub_menu_t menu; grub_menu_entry_t *last; + struct grub_menu_entry_class *classes; menu = grub_env_get_data_slot("menu"); if (! menu) @@ -173,6 +292,14 @@ return grub_errno; } + classes = get_classes_from_entry_title (menutitle); + if (! classes) + { + grub_free ((void *) menutitle); + grub_free ((void *) menusourcecode); + return grub_errno; + } + /* Add the menu entry at the end of the list. */ while (*last) last = &(*last)->next; @@ -180,6 +307,7 @@ *last = grub_malloc (sizeof (**last)); if (! *last) { + free_menu_entry_classes (classes); grub_free ((void *) menutitle); grub_free ((void *) menusourcecode); return grub_errno; @@ -187,6 +315,7 @@ (*last)->commands = script; (*last)->title = menutitle; + (*last)->classes = classes; (*last)->next = 0; (*last)->sourcecode = menusourcecode; @@ -476,7 +605,7 @@ if (menu && menu->size) { - grub_menu_run (menu, nested); + grub_menu_viewer_show_menu (menu, nested); if (nested) free_menu (menu); } @@ -519,6 +648,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 +666,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/bitmap_scale.c' --- video/bitmap_scale.c 2008-07-03 14:27:43 +0000 +++ video/bitmap_scale.c 2008-08-03 01:30:02 +0000 @@ -43,6 +43,7 @@ enum grub_video_bitmap_scale_method scale_method) { + *dst = 0; /* Verify the simplifying assumptions. */ if (src == 0) @@ -94,6 +95,7 @@ { /* Destroy the bitmap and return the error code. */ grub_video_bitmap_destroy (*dst); + *dst = 0; return ret; } } === 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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 QYiXQu7METCAxAx/+C7kinChIOGwa+o=