# 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=