720 lines
19 KiB
C
720 lines
19 KiB
C
/* theme_loader.c - Theme file loader for 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <grub/types.h>
|
|
#include <grub/file.h>
|
|
#include <grub/misc.h>
|
|
#include <grub/mm.h>
|
|
#include <grub/err.h>
|
|
#include <grub/dl.h>
|
|
#include <grub/video.h>
|
|
#include <grub/gui_string_util.h>
|
|
#include <grub/bitmap.h>
|
|
#include <grub/bitmap_scale.h>
|
|
#include <grub/gfxwidgets.h>
|
|
#include <grub/gfxmenu_view.h>
|
|
#include <grub/gui.h>
|
|
|
|
/* Construct a new box widget using ABSPATTERN to find the pixmap files for
|
|
it, storing the new box instance at *BOXPTR.
|
|
PATTERN should be of the form: "(hd0,0)/somewhere/style*.png".
|
|
The '*' then gets substituted with the various pixmap names that the
|
|
box uses. */
|
|
static grub_err_t
|
|
recreate_box_absolute (grub_gfxmenu_box_t *boxptr, const char *abspattern)
|
|
{
|
|
char *prefix;
|
|
char *suffix;
|
|
char *star;
|
|
grub_gfxmenu_box_t box;
|
|
|
|
star = grub_strchr (abspattern, '*');
|
|
if (! star)
|
|
return grub_error (GRUB_ERR_BAD_ARGUMENT,
|
|
"missing `*' in box pixmap pattern `%s'", abspattern);
|
|
|
|
/* Prefix: Get the part before the '*'. */
|
|
prefix = grub_malloc (star - abspattern + 1);
|
|
if (! prefix)
|
|
return grub_errno;
|
|
|
|
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 (prefix);
|
|
if (! box)
|
|
return grub_errno;
|
|
|
|
if (*boxptr)
|
|
(*boxptr)->destroy (*boxptr);
|
|
*boxptr = box;
|
|
return grub_errno;
|
|
}
|
|
|
|
|
|
/* 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.
|
|
|
|
Important! The value of *BOXPTR must be initialized! It must either
|
|
(1) Be 0 (a NULL pointer), or
|
|
(2) Be a pointer to a valid 'grub_gfxmenu_box_t' instance.
|
|
In this case, the previous instance is destroyed. */
|
|
grub_err_t
|
|
grub_gui_recreate_box (grub_gfxmenu_box_t *boxptr,
|
|
const char *pattern, const char *theme_dir)
|
|
{
|
|
char *abspattern;
|
|
|
|
/* Check arguments. */
|
|
if (! pattern)
|
|
{
|
|
/* If no pixmap pattern is given, then just create an empty box. */
|
|
if (*boxptr)
|
|
(*boxptr)->destroy (*boxptr);
|
|
*boxptr = grub_gfxmenu_create_box (0, 0);
|
|
return grub_errno;
|
|
}
|
|
|
|
if (! theme_dir)
|
|
return grub_error (GRUB_ERR_BAD_ARGUMENT,
|
|
"styled box missing theme directory");
|
|
|
|
/* Resolve to an absolute path. */
|
|
abspattern = grub_resolve_relative_path (theme_dir, pattern);
|
|
if (! abspattern)
|
|
return grub_errno;
|
|
|
|
/* Create the box. */
|
|
recreate_box_absolute (boxptr, abspattern);
|
|
grub_free (abspattern);
|
|
return grub_errno;
|
|
}
|
|
|
|
/* Set the specified property NAME on the view to the given string VALUE.
|
|
The caller is responsible for the lifetimes of NAME and VALUE. */
|
|
static grub_err_t
|
|
theme_set_string (grub_gfxmenu_view_t view,
|
|
const char *name,
|
|
const char *value,
|
|
const char *theme_dir,
|
|
const char *filename,
|
|
int line_num,
|
|
int col_num)
|
|
{
|
|
if (! grub_strcmp ("title-font", name))
|
|
view->title_font = grub_font_get (value);
|
|
else if (! grub_strcmp ("message-font", name))
|
|
view->message_font = grub_font_get (value);
|
|
else if (! grub_strcmp ("terminal-font", name))
|
|
{
|
|
grub_free (view->terminal_font_name);
|
|
view->terminal_font_name = grub_strdup (value);
|
|
if (! view->terminal_font_name)
|
|
return grub_errno;
|
|
}
|
|
else if (! grub_strcmp ("title-color", name))
|
|
grub_gui_parse_color (value, &view->title_color);
|
|
else if (! grub_strcmp ("message-color", name))
|
|
grub_gui_parse_color (value, &view->message_color);
|
|
else if (! grub_strcmp ("message-bg-color", name))
|
|
grub_gui_parse_color (value, &view->message_bg_color);
|
|
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)
|
|
return grub_errno;
|
|
if (grub_video_bitmap_load (&raw_bitmap, path) != GRUB_ERR_NONE)
|
|
{
|
|
grub_free (path);
|
|
return grub_errno;
|
|
}
|
|
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)
|
|
{
|
|
grub_error_push ();
|
|
return grub_error (grub_errno, "error scaling desktop image");
|
|
}
|
|
|
|
grub_video_bitmap_destroy (view->desktop_image);
|
|
view->desktop_image = scaled_bitmap;
|
|
}
|
|
else if (! grub_strcmp ("desktop-color", name))
|
|
grub_gui_parse_color (value, &view->desktop_color);
|
|
else if (! grub_strcmp ("terminal-box", name))
|
|
{
|
|
grub_err_t err;
|
|
err = grub_gui_recreate_box (&view->terminal_box, value, theme_dir);
|
|
if (err != GRUB_ERR_NONE)
|
|
return err;
|
|
}
|
|
else if (! grub_strcmp ("title-text", name))
|
|
{
|
|
grub_free (view->title_text);
|
|
view->title_text = grub_strdup (value);
|
|
if (! view->title_text)
|
|
return grub_errno;
|
|
}
|
|
else
|
|
{
|
|
return grub_error (GRUB_ERR_BAD_ARGUMENT,
|
|
"%s:%d:%d unknown property `%s'",
|
|
filename, line_num, col_num, name);
|
|
}
|
|
return grub_errno;
|
|
}
|
|
|
|
struct parsebuf
|
|
{
|
|
char *buf;
|
|
int pos;
|
|
int len;
|
|
int line_num;
|
|
int col_num;
|
|
const char *filename;
|
|
char *theme_dir;
|
|
grub_gfxmenu_view_t view;
|
|
};
|
|
|
|
static int
|
|
has_more (struct parsebuf *p)
|
|
{
|
|
return p->pos < p->len;
|
|
}
|
|
|
|
static int
|
|
read_char (struct parsebuf *p)
|
|
{
|
|
if (has_more (p))
|
|
{
|
|
char c;
|
|
c = p->buf[p->pos++];
|
|
if (c == '\n')
|
|
{
|
|
p->line_num++;
|
|
p->col_num = 1;
|
|
}
|
|
else
|
|
{
|
|
p->col_num++;
|
|
}
|
|
return c;
|
|
}
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
peek_char (struct parsebuf *p)
|
|
{
|
|
if (has_more (p))
|
|
return p->buf[p->pos];
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
is_whitespace (char c)
|
|
{
|
|
return (c == ' '
|
|
|| c == '\t'
|
|
|| c == '\r'
|
|
|| c == '\n'
|
|
|| c == '\f');
|
|
}
|
|
|
|
static void
|
|
skip_whitespace (struct parsebuf *p)
|
|
{
|
|
while (has_more (p) && is_whitespace(peek_char (p)))
|
|
read_char (p);
|
|
}
|
|
|
|
static void
|
|
advance_to_next_line (struct parsebuf *p)
|
|
{
|
|
int c;
|
|
|
|
/* Eat characters up to the newline. */
|
|
do
|
|
{
|
|
c = read_char (p);
|
|
}
|
|
while (c != -1 && c != '\n');
|
|
}
|
|
|
|
static int
|
|
is_identifier_char (int c)
|
|
{
|
|
return (c != -1
|
|
&& (grub_isalpha(c)
|
|
|| grub_isdigit(c)
|
|
|| c == '_'
|
|
|| c == '-'));
|
|
}
|
|
|
|
static char *
|
|
read_identifier (struct parsebuf *p)
|
|
{
|
|
/* Index of the first character of the identifier in p->buf. */
|
|
int start;
|
|
/* Next index after the last character of the identifer in p->buf. */
|
|
int end;
|
|
|
|
skip_whitespace (p);
|
|
|
|
/* Capture the start of the identifier. */
|
|
start = p->pos;
|
|
|
|
/* Scan for the end. */
|
|
while (is_identifier_char (peek_char (p)))
|
|
read_char (p);
|
|
end = p->pos;
|
|
|
|
if (end - start < 1)
|
|
return 0;
|
|
|
|
return grub_new_substring (p->buf, start, end);
|
|
}
|
|
|
|
static char *
|
|
read_expression (struct parsebuf *p)
|
|
{
|
|
int start;
|
|
int end;
|
|
|
|
skip_whitespace (p);
|
|
if (peek_char (p) == '"')
|
|
{
|
|
/* Read as a quoted string.
|
|
The quotation marks are not included in the expression value. */
|
|
/* Skip opening quotation mark. */
|
|
read_char (p);
|
|
start = p->pos;
|
|
while (has_more (p) && peek_char (p) != '"')
|
|
read_char (p);
|
|
end = p->pos;
|
|
/* Skip the terminating quotation mark. */
|
|
read_char (p);
|
|
}
|
|
else if (peek_char (p) == '(')
|
|
{
|
|
/* Read as a parenthesized string -- for tuples/coordinates. */
|
|
/* The parentheses are included in the expression value. */
|
|
int c;
|
|
|
|
start = p->pos;
|
|
do
|
|
{
|
|
c = read_char (p);
|
|
}
|
|
while (c != -1 && c != ')');
|
|
end = p->pos;
|
|
}
|
|
else if (has_more (p))
|
|
{
|
|
/* Read as a single word -- for numeric values or words without
|
|
whitespace. */
|
|
start = p->pos;
|
|
while (has_more (p) && ! is_whitespace (peek_char (p)))
|
|
read_char (p);
|
|
end = p->pos;
|
|
}
|
|
else
|
|
{
|
|
/* The end of the theme file has been reached. */
|
|
grub_error (GRUB_ERR_IO, "%s:%d:%d expression expected in theme file",
|
|
p->filename, p->line_num, p->col_num);
|
|
return 0;
|
|
}
|
|
|
|
return grub_new_substring (p->buf, start, end);
|
|
}
|
|
|
|
/* Read a GUI object specification from the theme file.
|
|
Any components created will be added to the GUI container PARENT. */
|
|
static grub_err_t
|
|
read_object (struct parsebuf *p, grub_gui_container_t parent)
|
|
{
|
|
grub_video_rect_t bounds;
|
|
|
|
char *name;
|
|
name = read_identifier (p);
|
|
if (! name)
|
|
goto cleanup;
|
|
|
|
grub_gui_component_t component = 0;
|
|
if (grub_strcmp (name, "label") == 0)
|
|
{
|
|
component = grub_gui_label_new ();
|
|
}
|
|
else if (grub_strcmp (name, "image") == 0)
|
|
{
|
|
component = grub_gui_image_new ();
|
|
}
|
|
else if (grub_strcmp (name, "vbox") == 0)
|
|
{
|
|
component = (grub_gui_component_t) grub_gui_vbox_new ();
|
|
}
|
|
else if (grub_strcmp (name, "hbox") == 0)
|
|
{
|
|
component = (grub_gui_component_t) grub_gui_hbox_new ();
|
|
}
|
|
else if (grub_strcmp (name, "canvas") == 0)
|
|
{
|
|
component = (grub_gui_component_t) grub_gui_canvas_new ();
|
|
}
|
|
else if (grub_strcmp (name, "progress_bar") == 0)
|
|
{
|
|
component = grub_gui_progress_bar_new ();
|
|
}
|
|
else if (grub_strcmp (name, "circular_progress") == 0)
|
|
{
|
|
component = grub_gui_circular_progress_new ();
|
|
}
|
|
else if (grub_strcmp (name, "boot_menu") == 0)
|
|
{
|
|
component = grub_gui_list_new ();
|
|
}
|
|
else
|
|
{
|
|
/* Unknown type. */
|
|
grub_error (GRUB_ERR_IO, "%s:%d:%d unknown object type `%s'",
|
|
p->filename, p->line_num, p->col_num, name);
|
|
goto cleanup;
|
|
}
|
|
|
|
if (! component)
|
|
goto cleanup;
|
|
|
|
/* Inform the component about the theme so it can find its resources. */
|
|
component->ops->set_property (component, "theme_dir", p->theme_dir);
|
|
component->ops->set_property (component, "theme_path", p->filename);
|
|
|
|
/* Add the component as a child of PARENT. */
|
|
bounds.x = 0;
|
|
bounds.y = 0;
|
|
bounds.width = -1;
|
|
bounds.height = -1;
|
|
component->ops->set_bounds (component, &bounds);
|
|
parent->ops->add (parent, component);
|
|
|
|
skip_whitespace (p);
|
|
if (read_char (p) != '{')
|
|
{
|
|
grub_error (GRUB_ERR_IO,
|
|
"%s:%d:%d expected `{' after object type name `%s'",
|
|
p->filename, p->line_num, p->col_num, name);
|
|
goto cleanup;
|
|
}
|
|
|
|
while (has_more (p))
|
|
{
|
|
skip_whitespace (p);
|
|
|
|
/* Check whether the end has been encountered. */
|
|
if (peek_char (p) == '}')
|
|
{
|
|
/* Skip the closing brace. */
|
|
read_char (p);
|
|
break;
|
|
}
|
|
|
|
if (peek_char (p) == '#')
|
|
{
|
|
/* Skip comments. */
|
|
advance_to_next_line (p);
|
|
continue;
|
|
}
|
|
|
|
if (peek_char (p) == '+')
|
|
{
|
|
/* Skip the '+'. */
|
|
read_char (p);
|
|
|
|
/* Check whether this component is a container. */
|
|
if (component->ops->is_instance (component, "container"))
|
|
{
|
|
/* Read the sub-object recursively and add it as a child. */
|
|
if (read_object (p, (grub_gui_container_t) component) != 0)
|
|
goto cleanup;
|
|
/* After reading the sub-object, resume parsing, expecting
|
|
another property assignment or sub-object definition. */
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
grub_error (GRUB_ERR_IO,
|
|
"%s:%d:%d attempted to add object to non-container",
|
|
p->filename, p->line_num, p->col_num);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
char *property;
|
|
property = read_identifier (p);
|
|
if (! property)
|
|
{
|
|
grub_error (GRUB_ERR_IO, "%s:%d:%d identifier expected in theme file",
|
|
p->filename, p->line_num, p->col_num);
|
|
goto cleanup;
|
|
}
|
|
|
|
skip_whitespace (p);
|
|
if (read_char (p) != '=')
|
|
{
|
|
grub_error (GRUB_ERR_IO,
|
|
"%s:%d:%d expected `=' after property name `%s'",
|
|
p->filename, p->line_num, p->col_num, property);
|
|
grub_free (property);
|
|
goto cleanup;
|
|
}
|
|
skip_whitespace (p);
|
|
|
|
char *value;
|
|
value = read_expression (p);
|
|
if (! value)
|
|
{
|
|
grub_free (property);
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Handle the property value. */
|
|
if (grub_strcmp (property, "position") == 0)
|
|
{
|
|
/* Special case for position value. */
|
|
int x;
|
|
int y;
|
|
|
|
if (grub_gui_parse_2_tuple (value, &x, &y) == GRUB_ERR_NONE)
|
|
{
|
|
grub_video_rect_t r;
|
|
component->ops->get_bounds (component, &r);
|
|
r.x = x;
|
|
r.y = y;
|
|
component->ops->set_bounds (component, &r);
|
|
}
|
|
}
|
|
else if (grub_strcmp (property, "size") == 0)
|
|
{
|
|
/* Special case for size value. */
|
|
int w;
|
|
int h;
|
|
|
|
if (grub_gui_parse_2_tuple (value, &w, &h) == GRUB_ERR_NONE)
|
|
{
|
|
grub_video_rect_t r;
|
|
component->ops->get_bounds (component, &r);
|
|
r.width = w;
|
|
r.height = h;
|
|
component->ops->set_bounds (component, &r);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* General property handling. */
|
|
component->ops->set_property (component, property, value);
|
|
}
|
|
|
|
grub_free (value);
|
|
grub_free (property);
|
|
if (grub_errno != GRUB_ERR_NONE)
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Set the object's size to its preferred size unless the user has
|
|
explicitly specified the size. */
|
|
component->ops->get_bounds (component, &bounds);
|
|
if (bounds.width == -1 || bounds.height == -1)
|
|
{
|
|
component->ops->get_preferred_size (component,
|
|
&bounds.width, &bounds.height);
|
|
component->ops->set_bounds (component, &bounds);
|
|
}
|
|
|
|
cleanup:
|
|
grub_free (name);
|
|
return grub_errno;
|
|
}
|
|
|
|
static grub_err_t
|
|
read_property (struct parsebuf *p)
|
|
{
|
|
char *name;
|
|
|
|
/* Read the property name. */
|
|
name = read_identifier (p);
|
|
if (! name)
|
|
{
|
|
advance_to_next_line (p);
|
|
return grub_errno;
|
|
}
|
|
|
|
/* Skip whitespace before separator. */
|
|
skip_whitespace (p);
|
|
|
|
/* Read separator. */
|
|
if (read_char (p) != ':')
|
|
{
|
|
grub_error (GRUB_ERR_IO,
|
|
"%s:%d:%d missing separator after property name `%s'",
|
|
p->filename, p->line_num, p->col_num, name);
|
|
goto done;
|
|
}
|
|
|
|
/* Skip whitespace after separator. */
|
|
skip_whitespace (p);
|
|
|
|
/* Get the value based on its type. */
|
|
if (peek_char (p) == '"')
|
|
{
|
|
/* String value (e.g., '"My string"'). */
|
|
char *value = read_expression (p);
|
|
if (! value)
|
|
{
|
|
grub_error (GRUB_ERR_IO, "%s:%d:%d missing property value",
|
|
p->filename, p->line_num, p->col_num);
|
|
goto done;
|
|
}
|
|
/* If theme_set_string results in an error, grub_errno will be returned
|
|
below. */
|
|
theme_set_string (p->view, name, value, p->theme_dir,
|
|
p->filename, p->line_num, p->col_num);
|
|
grub_free (value);
|
|
}
|
|
else
|
|
{
|
|
grub_error (GRUB_ERR_IO,
|
|
"%s:%d:%d property value invalid; "
|
|
"enclose literal values in quotes (\")",
|
|
p->filename, p->line_num, p->col_num);
|
|
goto done;
|
|
}
|
|
|
|
done:
|
|
grub_free (name);
|
|
return grub_errno;
|
|
}
|
|
|
|
/* Set properties on the view based on settings from the specified
|
|
theme file. */
|
|
grub_err_t
|
|
grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, const char *theme_path)
|
|
{
|
|
grub_file_t file;
|
|
struct parsebuf p;
|
|
|
|
p.view = view;
|
|
p.theme_dir = grub_get_dirname (theme_path);
|
|
|
|
file = grub_file_open (theme_path);
|
|
if (! file)
|
|
{
|
|
grub_free (p.theme_dir);
|
|
return grub_errno;
|
|
}
|
|
|
|
p.len = grub_file_size (file);
|
|
p.buf = grub_malloc (p.len);
|
|
p.pos = 0;
|
|
p.line_num = 1;
|
|
p.col_num = 1;
|
|
p.filename = theme_path;
|
|
if (! p.buf)
|
|
{
|
|
grub_file_close (file);
|
|
grub_free (p.theme_dir);
|
|
return grub_errno;
|
|
}
|
|
if (grub_file_read (file, p.buf, p.len) != p.len)
|
|
{
|
|
grub_free (p.buf);
|
|
grub_file_close (file);
|
|
grub_free (p.theme_dir);
|
|
return grub_errno;
|
|
}
|
|
|
|
if (view->canvas)
|
|
view->canvas->ops->component.destroy (view->canvas);
|
|
|
|
view->canvas = grub_gui_canvas_new ();
|
|
((grub_gui_component_t) view->canvas)
|
|
->ops->set_bounds ((grub_gui_component_t) view->canvas,
|
|
&view->screen);
|
|
|
|
while (has_more (&p))
|
|
{
|
|
/* Skip comments (lines beginning with #). */
|
|
if (peek_char (&p) == '#')
|
|
{
|
|
advance_to_next_line (&p);
|
|
continue;
|
|
}
|
|
|
|
/* Find the first non-whitespace character. */
|
|
skip_whitespace (&p);
|
|
|
|
/* Handle the content. */
|
|
if (peek_char (&p) == '+')
|
|
{
|
|
/* Skip the '+'. */
|
|
read_char (&p);
|
|
read_object (&p, view->canvas);
|
|
}
|
|
else
|
|
{
|
|
read_property (&p);
|
|
}
|
|
|
|
if (grub_errno != GRUB_ERR_NONE)
|
|
goto fail;
|
|
}
|
|
|
|
/* Set the new theme path. */
|
|
grub_free (view->theme_path);
|
|
view->theme_path = grub_strdup (theme_path);
|
|
goto cleanup;
|
|
|
|
fail:
|
|
if (view->canvas)
|
|
{
|
|
view->canvas->ops->component.destroy (view->canvas);
|
|
view->canvas = 0;
|
|
}
|
|
|
|
cleanup:
|
|
grub_free (p.buf);
|
|
grub_file_close (file);
|
|
grub_free (p.theme_dir);
|
|
return grub_errno;
|
|
}
|