/* * st-theme-node.c: style information for one node in a tree of themed objects * * Copyright 2008-2010 Red Hat, Inc. * Copyright 2009 Steve Frécinaux * Copyright 2009, 2010 Florian Müllner * Copyright 2010 Adel Gadllah * Copyright 2010 Giovanni Campagna * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 2.1 of * the License, or (at your option) any later version. * * This program is distributed in the hope it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* HAVE_CONFIG_H */ #include #include #include #include "eek-theme-node.h" #include "eek-theme-private.h" G_DEFINE_TYPE (EekThemeNode, eek_theme_node, G_TYPE_OBJECT); #define EEK_THEME_NODE_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), EEK_TYPE_THEME_NODE, EekThemeNodePrivate)) struct _EekThemeNodePrivate { EekThemeNode *parent_node; EekTheme *theme; EekColor *transparent_color; EekColor *black_color; EekGradientType background_gradient_type; EekColor *background_color; EekColor *background_gradient_end; EekColor *foreground_color; GType element_type; char *element_id; char *element_class; char *pseudo_class; char *inline_style; CRDeclaration **properties; int n_properties; /* We hold onto these separately so we can destroy them on finalize */ CRDeclaration *inline_properties; guint properties_computed : 1; guint background_computed : 1; guint foreground_computed : 1; }; static void eek_theme_node_dispose (GObject *gobject) { EekThemeNodePrivate *priv = EEK_THEME_NODE_GET_PRIVATE (gobject); if (priv->theme) { g_object_unref (priv->theme); priv->theme = NULL; } if (priv->parent_node) { g_object_unref (priv->parent_node); priv->parent_node = NULL; } G_OBJECT_CLASS (eek_theme_node_parent_class)->dispose (gobject); } static void eek_theme_node_finalize (GObject *object) { EekThemeNodePrivate *priv = EEK_THEME_NODE_GET_PRIVATE (object); eek_color_free (priv->transparent_color); eek_color_free (priv->black_color); eek_color_free (priv->background_color); eek_color_free (priv->background_gradient_end); eek_color_free (priv->foreground_color); g_free (priv->element_id); g_free (priv->element_class); g_free (priv->pseudo_class); g_free (priv->inline_style); if (priv->properties) { g_free (priv->properties); priv->properties = NULL; priv->n_properties = 0; } if (priv->inline_properties) { /* This destroys the list, not just the head of the list */ cr_declaration_destroy (priv->inline_properties); } G_OBJECT_CLASS (eek_theme_node_parent_class)->finalize (object); } static void eek_theme_node_class_init (EekThemeNodeClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); g_type_class_add_private (gobject_class, sizeof (EekThemeNodePrivate)); gobject_class->dispose = eek_theme_node_dispose; gobject_class->finalize = eek_theme_node_finalize; } static void eek_theme_node_init (EekThemeNode *self) { EekThemeNodePrivate *priv; priv = self->priv = EEK_THEME_NODE_GET_PRIVATE(self); priv->transparent_color = eek_color_new (0.0, 0.0, 0.0, 0.0); priv->black_color = eek_color_new (0.0, 0.0, 0.0, 1.0); priv->background_color = eek_color_copy (priv->transparent_color); priv->background_gradient_end = eek_color_copy (priv->transparent_color); priv->foreground_color = eek_color_copy (priv->black_color); priv->background_gradient_type = EEK_GRADIENT_NONE; } /** * eek_theme_node_new: * @parent_node: (allow-none): the parent node of this node * @theme: (allow-none): a theme (stylesheet set) that overrides the * theme inherited from the parent node * @element_type: the type of the GObject represented by this node * in the tree (corresponding to an element if we were theming an XML * document. %G_TYPE_NONE means this style was created for the stage * actor and matches a selector element name of 'stage'. * @element_id: (allow-none): the ID to match CSS rules against * @element_class: (allow-none): a whitespace-separated list of classes * to match CSS rules against * @pseudo_class: (allow-none): a whitespace-separated list of pseudo-classes * (like 'hover' or 'visited') to match CSS rules against * * Creates a new #EekThemeNode. Once created, a node is immutable. Of any * of the attributes of the node (like the @element_class) change the node * and its child nodes must be destroyed and recreated. * * Return value: (transfer full): the theme node */ EekThemeNode * eek_theme_node_new (EekThemeNode *parent_node, EekTheme *theme, GType element_type, const char *element_id, const char *element_class, const char *pseudo_class, const char *inline_style) { EekThemeNode *node; EekThemeNodePrivate *priv; g_return_val_if_fail (parent_node == NULL || EEK_IS_THEME_NODE (parent_node), NULL); node = g_object_new (EEK_TYPE_THEME_NODE, NULL); priv = EEK_THEME_NODE_GET_PRIVATE(node); if (parent_node != NULL) priv->parent_node = g_object_ref (parent_node); else priv->parent_node = NULL; if (theme == NULL && parent_node != NULL) theme = eek_theme_node_get_theme (parent_node); if (theme != NULL) priv->theme = g_object_ref (theme); priv->element_type = element_type; priv->element_id = g_strdup (element_id); priv->element_class = g_strdup (element_class); priv->pseudo_class = g_strdup (pseudo_class); priv->inline_style = g_strdup (inline_style); return node; } /** * eek_theme_node_get_parent: * @node: a #EekThemeNode * * Gets the parent themed element node. * * Return value: (transfer none): the parent #EekThemeNode, or %NULL if this * is the root node of the tree of theme elements. */ EekThemeNode * eek_theme_node_get_parent (EekThemeNode *node) { EekThemeNodePrivate *priv; g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); priv = EEK_THEME_NODE_GET_PRIVATE(node); return priv->parent_node; } /** * eek_theme_node_get_theme: * @node: a #EekThemeNode * * Gets the theme stylesheet set that styles this node * * Return value: (transfer none): the theme stylesheet set */ EekTheme * eek_theme_node_get_theme (EekThemeNode *node) { EekThemeNodePrivate *priv; g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); priv = EEK_THEME_NODE_GET_PRIVATE(node); return priv->theme; } GType eek_theme_node_get_element_type (EekThemeNode *node) { EekThemeNodePrivate *priv; g_return_val_if_fail (EEK_IS_THEME_NODE (node), G_TYPE_NONE); priv = EEK_THEME_NODE_GET_PRIVATE(node); return priv->element_type; } const char * eek_theme_node_get_element_id (EekThemeNode *node) { EekThemeNodePrivate *priv; g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); priv = EEK_THEME_NODE_GET_PRIVATE(node); return priv->element_id; } const char * eek_theme_node_get_element_class (EekThemeNode *node) { EekThemeNodePrivate *priv; g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); priv = EEK_THEME_NODE_GET_PRIVATE(node); return priv->element_class; } const char * eek_theme_node_get_pseudo_class (EekThemeNode *node) { EekThemeNodePrivate *priv; g_return_val_if_fail (EEK_IS_THEME_NODE (node), NULL); priv = EEK_THEME_NODE_GET_PRIVATE(node); return priv->pseudo_class; } static void ensure_properties (EekThemeNode *node) { EekThemeNodePrivate *priv = EEK_THEME_NODE_GET_PRIVATE(node); if (!priv->properties_computed) { GPtrArray *properties = NULL; priv->properties_computed = TRUE; if (priv->theme) properties = _eek_theme_get_matched_properties (priv->theme, node); if (priv->inline_style) { CRDeclaration *cur_decl; if (!properties) properties = g_ptr_array_new (); priv->inline_properties = _eek_theme_parse_declaration_list (priv->inline_style); for (cur_decl = priv->inline_properties; cur_decl; cur_decl = cur_decl->next) g_ptr_array_add (properties, cur_decl); } if (properties) { priv->n_properties = properties->len; priv->properties = (CRDeclaration **)g_ptr_array_free (properties, FALSE); } } } typedef enum { VALUE_FOUND, VALUE_NOT_FOUND, VALUE_INHERIT } GetFromTermResult; static gboolean term_is_none (CRTerm *term) { return (term->type == TERM_IDENT && strcmp (term->content.str->stryng->str, "none") == 0); } static gboolean term_is_transparent (CRTerm *term) { return (term->type == TERM_IDENT && strcmp (term->content.str->stryng->str, "transparent") == 0); } static GetFromTermResult get_color_from_rgba_term (CRTerm *term, EekColor **color) { CRTerm *arg = term->ext_content.func_param; CRNum *num; double r = 0, g = 0, b = 0, a = 0; int i; for (i = 0; i < 4; i++) { double value; if (arg == NULL) return VALUE_NOT_FOUND; if ((i == 0 && arg->the_operator != NO_OP) || (i > 0 && arg->the_operator != COMMA)) return VALUE_NOT_FOUND; if (arg->type != TERM_NUMBER) return VALUE_NOT_FOUND; num = arg->content.num; /* For simplicity, we convert a,r,g,b to [0,1.0] floats and then * convert them back below. Then when we set them on a cairo content * we convert them back to floats, and then cairo converts them * back to integers to pass them to X, and so forth... */ if (i < 3) { if (num->type == NUM_PERCENTAGE) value = num->val / 100; else if (num->type == NUM_GENERIC) value = num->val / 255; else return VALUE_NOT_FOUND; } else { if (num->type != NUM_GENERIC) return VALUE_NOT_FOUND; value = num->val; } value = CLAMP (value, 0, 1); switch (i) { case 0: r = value; break; case 1: g = value; break; case 2: b = value; break; case 3: a = value; break; } arg = arg->next; } *color = eek_color_new (CLAMP(r, 0.0, 1.0), CLAMP(g, 0.0, 1.0), CLAMP(b, 0.0, 1.0), CLAMP(a, 0.0, 1.0)); return VALUE_FOUND; } static GetFromTermResult get_color_from_term (EekThemeNode *node, CRTerm *term, EekColor **color) { EekThemeNodePrivate *priv = EEK_THEME_NODE_GET_PRIVATE(node); CRRgb rgb; enum CRStatus status; /* Since libcroco doesn't know about rgba colors, it can't handle * the transparent keyword */ if (term_is_transparent (term)) { *color = eek_color_copy (priv->transparent_color); return VALUE_FOUND; } /* rgba () colors - a CSS3 addition, are not supported by libcroco, * but they are parsed as a "function", so we can emulate the * functionality. * * libcroco < 0.6.2 has a bug where functions starting with 'r' are * misparsed. We workaround this by pre-converting 'rgba' to 'RGBA' * before parsing the stylesheet. Since libcroco isn't * case-insensitive (a bug), it's fine with functions starting with * 'R'. (In theory, we should be doing a case-insensitive compare * everywhere, not just here, but that doesn't make much sense when * the built-in parsing of libcroco is case-sensitive and things * like 10PX don't work.) */ else if (term->type == TERM_FUNCTION && term->content.str && term->content.str->stryng && term->content.str->stryng->str && g_ascii_strcasecmp (term->content.str->stryng->str, "rgba") == 0) { return get_color_from_rgba_term (term, color); } status = cr_rgb_set_from_term (&rgb, term); if (status != CR_OK) return VALUE_NOT_FOUND; if (rgb.inherit) return VALUE_INHERIT; if (rgb.is_percentage) cr_rgb_compute_from_percentage (&rgb); *color = eek_color_new (rgb.red / (gdouble)0xff, rgb.green / (gdouble)0xff, rgb.blue / (gdouble)0xff, 1.0); return VALUE_FOUND; } /** * eek_theme_node_get_color: * @node: a #EekThemeNode * @property_name: The name of the color property * @inherit: if %TRUE, if a value is not found for the property on the * node, then it will be looked up on the parent node, and then on the * parent's parent, and so forth. Note that if the property has a * value of 'inherit' it will be inherited even if %FALSE is passed * in for @inherit; this only affects the default behavior for inheritance. * @color: location to store the color that was determined. * If the property is not found, the value in this location * will not be changed. * * Generically looks up a property containing a single color value. When * specific getters (like eek_theme_node_get_background_color()) exist, they * should be used instead. They are cached, so more efficient, and have * handling for shortcut properties and other details of CSS. * * Return value: %TRUE if the property was found in the properties for this * theme node (or in the properties of parent nodes when inheriting.) */ gboolean eek_theme_node_get_color (EekThemeNode *node, const char *property_name, gboolean inherit, EekColor **color) { EekThemeNodePrivate *priv; int i; g_return_val_if_fail (EEK_IS_THEME_NODE(node), FALSE); priv = EEK_THEME_NODE_GET_PRIVATE(node); ensure_properties (node); for (i = priv->n_properties - 1; i >= 0; i--) { CRDeclaration *decl = priv->properties[i]; if (strcmp (decl->property->stryng->str, property_name) == 0) { GetFromTermResult result = get_color_from_term (node, decl->value, color); if (result == VALUE_FOUND) { return TRUE; } else if (result == VALUE_INHERIT) { if (priv->parent_node) return eek_theme_node_get_color (priv->parent_node, property_name, inherit, color); else break; } } } return FALSE; } static GetFromTermResult get_background_color_from_term (EekThemeNode *node, CRTerm *term, EekColor **color) { EekThemeNodePrivate *priv = EEK_THEME_NODE_GET_PRIVATE(node); GetFromTermResult result = get_color_from_term (node, term, color); if (result == VALUE_NOT_FOUND) { if (term_is_transparent (term)) { *color = eek_color_copy (priv->transparent_color); return VALUE_FOUND; } } return result; } void _eek_theme_node_ensure_background (EekThemeNode *node) { EekThemeNodePrivate *priv = EEK_THEME_NODE_GET_PRIVATE(node); int i; if (priv->background_computed) return; priv->background_computed = TRUE; eek_color_free (priv->background_color); priv->background_color = eek_color_copy (priv->transparent_color); eek_color_free (priv->background_gradient_end); priv->background_gradient_end = eek_color_copy (priv->background_color); priv->background_gradient_type = EEK_GRADIENT_NONE; ensure_properties (node); for (i = 0; i < priv->n_properties; i++) { CRDeclaration *decl = priv->properties[i]; const char *property_name = decl->property->stryng->str; GetFromTermResult result; EekColor *color; if (g_str_has_prefix (property_name, "background")) property_name += 10; else continue; if (strcmp (property_name, "") == 0) { /* We're very liberal here ... if we recognize any term in * the expression we take it, and we ignore the rest. The * actual specification is: * * background: [<'background-color'> || * <'background-image'> || <'background-repeat'> || * <'background-attachment'> || <'background-position'>] | * inherit */ CRTerm *term; /* background: property sets all terms to specified or default values */ eek_color_free (priv->background_color); priv->background_color = eek_color_copy (priv->transparent_color); for (term = decl->value; term; term = term->next) { result = get_background_color_from_term (node, term, &color); if (result == VALUE_FOUND) { eek_color_free (priv->background_color); priv->background_color = color; } else if (result == VALUE_INHERIT) { if (priv->parent_node) { color = eek_theme_node_get_background_color (priv->parent_node); if (color) { eek_color_free (priv->background_color); priv->background_color = color; } } } else if (term_is_none (term)) { /* leave priv->background_color as transparent */ } } } else if (strcmp (property_name, "-color") == 0) { if (decl->value == NULL || decl->value->next != NULL) continue; result = get_background_color_from_term (node, decl->value, &color); if (result == VALUE_FOUND) { eek_color_free (priv->background_color); priv->background_color = color; } else if (result == VALUE_INHERIT) { if (priv->parent_node) { color = eek_theme_node_get_background_color (priv->parent_node); if (color) { eek_color_free (priv->background_color); priv->background_color = color; } } } } else if (strcmp (property_name, "-gradient-direction") == 0) { CRTerm *term = decl->value; if (strcmp (term->content.str->stryng->str, "vertical") == 0) { priv->background_gradient_type = EEK_GRADIENT_VERTICAL; } else if (strcmp (term->content.str->stryng->str, "horizontal") == 0) { priv->background_gradient_type = EEK_GRADIENT_HORIZONTAL; } else if (strcmp (term->content.str->stryng->str, "radial") == 0) { priv->background_gradient_type = EEK_GRADIENT_RADIAL; } else if (strcmp (term->content.str->stryng->str, "none") == 0) { priv->background_gradient_type = EEK_GRADIENT_NONE; } else { g_warning ("Unrecognized background-gradient-direction \"%s\"", term->content.str->stryng->str); } } else if (strcmp (property_name, "-gradient-start") == 0) { result = get_color_from_term (node, decl->value, &color); if (result == VALUE_FOUND) { eek_color_free (priv->background_color); priv->background_color = color; } } else if (strcmp (property_name, "-gradient-end") == 0) { result = get_color_from_term (node, decl->value, &color); if (result == VALUE_FOUND) { eek_color_free (priv->background_gradient_end); priv->background_gradient_end = color; } } } } EekColor * eek_theme_node_get_background_color (EekThemeNode *node) { EekThemeNodePrivate *priv; g_assert (EEK_IS_THEME_NODE (node)); priv = EEK_THEME_NODE_GET_PRIVATE(node); _eek_theme_node_ensure_background (node); if (priv->background_gradient_type == EEK_GRADIENT_NONE) return eek_color_copy (priv->background_color); return NULL; } EekColor * eek_theme_node_get_foreground_color (EekThemeNode *node) { EekThemeNodePrivate *priv; g_assert (EEK_IS_THEME_NODE (node)); priv = EEK_THEME_NODE_GET_PRIVATE(node); if (!priv->foreground_computed) { int i; EekColor *color; priv->foreground_computed = TRUE; ensure_properties (node); for (i = priv->n_properties - 1; i >= 0; i--) { CRDeclaration *decl = priv->properties[i]; if (strcmp (decl->property->stryng->str, "color") == 0) { GetFromTermResult result = get_color_from_term (node, decl->value, &color); if (result == VALUE_FOUND) { eek_color_free (priv->foreground_color); priv->foreground_color = color; goto out; } if (result == VALUE_INHERIT) break; } } if (priv->parent_node) { color = eek_theme_node_get_foreground_color (priv->parent_node); if (color) { eek_color_free (priv->foreground_color); priv->foreground_color = color; } } else { /* default to black */ eek_color_free (priv->foreground_color); priv->foreground_color = eek_color_copy (priv->black_color); } } out: return eek_color_copy (priv->foreground_color); } /** * eek_theme_node_get_background_gradient: * @node: A #EekThemeNode * * Get the background gradient of @node. If the node does not have * gradient property, returns %NULL. * * Returns: an #EekGradient, which should be freed with * eek_gradient_free() or %NULL. */ EekGradient * eek_theme_node_get_background_gradient (EekThemeNode *node) { EekThemeNodePrivate *priv; g_assert (EEK_IS_THEME_NODE (node)); priv = EEK_THEME_NODE_GET_PRIVATE(node); _eek_theme_node_ensure_background (node); if (priv->background_gradient_type == EEK_GRADIENT_NONE) return NULL; return eek_gradient_new (priv->background_gradient_type, priv->background_color, priv->background_gradient_end); }