9168f6055e
Much of St is undocumented, aside from input/output arguments. This is no doubt because a lot of it parallels Gtk closely, but is worth improving since many new programmers are not familiar with Gtk. closes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2983
1086 lines
32 KiB
C
1086 lines
32 KiB
C
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
|
|
/*
|
|
* st-theme.c: A set of CSS stylesheets used for rule matching
|
|
*
|
|
* Copyright 2003-2004 Dodji Seketeli
|
|
* Copyright 2008, 2009 Red Hat, Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms and conditions of the GNU Lesser General Public License,
|
|
* version 2.1, as published by the Free Software Foundation.
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* This file started as a cut-and-paste of cr-sel-eng.c from libcroco.
|
|
*
|
|
* In moving it to hippo-canvas:
|
|
* - Reformatted and otherwise edited to match our coding style
|
|
* - Switched from handling xmlNode to handling HippoStyle
|
|
* - Simplified by removing things that we don't need or that don't
|
|
* make sense in our context.
|
|
* - The code to get a list of matching properties works quite differently;
|
|
* we order things in priority order, but we don't actually try to
|
|
* coalesce properties with the same name.
|
|
*
|
|
* In moving it to GNOME Shell:
|
|
* - Renamed again to StTheme
|
|
* - Reformatted to match the gnome-shell coding style
|
|
* - Removed notion of "theme engine" from hippo-canvas
|
|
* - pseudo-class matching changed from link enum to strings
|
|
* - Some code simplification
|
|
*/
|
|
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <gio/gio.h>
|
|
|
|
#include "st-private.h"
|
|
#include "st-theme-node.h"
|
|
#include "st-theme-private.h"
|
|
|
|
static void st_theme_constructed (GObject *object);
|
|
static void st_theme_finalize (GObject *object);
|
|
static void st_theme_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec);
|
|
static void st_theme_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec);
|
|
|
|
struct _StTheme
|
|
{
|
|
GObject parent;
|
|
|
|
GFile *application_stylesheet;
|
|
GFile *default_stylesheet;
|
|
GFile *theme_stylesheet;
|
|
GSList *custom_stylesheets;
|
|
|
|
GHashTable *stylesheets_by_file;
|
|
GHashTable *files_by_stylesheet;
|
|
|
|
CRCascade *cascade;
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_APPLICATION_STYLESHEET,
|
|
PROP_THEME_STYLESHEET,
|
|
PROP_DEFAULT_STYLESHEET
|
|
};
|
|
|
|
enum
|
|
{
|
|
STYLESHEETS_CHANGED,
|
|
LAST_SIGNAL
|
|
};
|
|
|
|
static guint signals[LAST_SIGNAL] = { 0, };
|
|
|
|
G_DEFINE_TYPE (StTheme, st_theme, G_TYPE_OBJECT)
|
|
|
|
/* Quick strcmp. Test only for == 0 or != 0, not < 0 or > 0. */
|
|
#define strqcmp(str,lit,lit_len) \
|
|
(strlen (str) != (lit_len) || memcmp (str, lit, lit_len))
|
|
|
|
static gboolean
|
|
file_equal0 (GFile *file1,
|
|
GFile *file2)
|
|
{
|
|
if (file1 == file2)
|
|
return TRUE;
|
|
|
|
if ((file1 == NULL) || (file2 == NULL))
|
|
return FALSE;
|
|
|
|
return g_file_equal (file1, file2);
|
|
}
|
|
|
|
static void
|
|
st_theme_init (StTheme *theme)
|
|
{
|
|
theme->stylesheets_by_file = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal,
|
|
(GDestroyNotify)g_object_unref, (GDestroyNotify)cr_stylesheet_unref);
|
|
theme->files_by_stylesheet = g_hash_table_new (g_direct_hash, g_direct_equal);
|
|
}
|
|
|
|
static void
|
|
st_theme_class_init (StThemeClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->constructed = st_theme_constructed;
|
|
object_class->finalize = st_theme_finalize;
|
|
object_class->set_property = st_theme_set_property;
|
|
object_class->get_property = st_theme_get_property;
|
|
|
|
/**
|
|
* StTheme:application-stylesheet:
|
|
*
|
|
* The highest priority stylesheet, representing application-specific
|
|
* styling; this is associated with the CSS "author" stylesheet.
|
|
*/
|
|
g_object_class_install_property (object_class,
|
|
PROP_APPLICATION_STYLESHEET,
|
|
g_param_spec_object ("application-stylesheet",
|
|
"Application Stylesheet",
|
|
"Stylesheet with application-specific styling",
|
|
G_TYPE_FILE,
|
|
ST_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
/**
|
|
* StTheme:theme-stylesheet:
|
|
*
|
|
* The second priority stylesheet, representing theme-specific styling;
|
|
* this is associated with the CSS "user" stylesheet.
|
|
*/
|
|
g_object_class_install_property (object_class,
|
|
PROP_THEME_STYLESHEET,
|
|
g_param_spec_object ("theme-stylesheet",
|
|
"Theme Stylesheet",
|
|
"Stylesheet with theme-specific styling",
|
|
G_TYPE_FILE,
|
|
ST_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
/**
|
|
* StTheme:default-stylesheet:
|
|
*
|
|
* The lowest priority stylesheet, representing global default
|
|
* styling; this is associated with the CSS "user agent" stylesheet.
|
|
*/
|
|
g_object_class_install_property (object_class,
|
|
PROP_DEFAULT_STYLESHEET,
|
|
g_param_spec_object ("default-stylesheet",
|
|
"Default Stylesheet",
|
|
"Stylesheet with global default styling",
|
|
G_TYPE_FILE,
|
|
ST_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
signals[STYLESHEETS_CHANGED] =
|
|
g_signal_new ("custom-stylesheets-changed",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST,
|
|
0, /* no default handler slot */
|
|
NULL, NULL, NULL,
|
|
G_TYPE_NONE, 0);
|
|
}
|
|
|
|
static CRStyleSheet *
|
|
parse_stylesheet (GFile *file,
|
|
GError **error)
|
|
{
|
|
enum CRStatus status;
|
|
CRStyleSheet *stylesheet;
|
|
char *contents;
|
|
gsize length;
|
|
|
|
if (file == NULL)
|
|
return NULL;
|
|
|
|
if (!g_file_load_contents (file, NULL, &contents, &length, NULL, error))
|
|
return NULL;
|
|
|
|
status = cr_om_parser_simply_parse_buf ((const guchar *) contents,
|
|
length,
|
|
CR_UTF_8,
|
|
&stylesheet);
|
|
g_free (contents);
|
|
|
|
if (status != CR_OK)
|
|
{
|
|
char *uri = g_file_get_uri (file);
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Error parsing stylesheet '%s'; errcode:%d", uri, status);
|
|
g_free (uri);
|
|
return NULL;
|
|
}
|
|
|
|
/* Extension stylesheet */
|
|
stylesheet->app_data = GUINT_TO_POINTER (FALSE);
|
|
|
|
return stylesheet;
|
|
}
|
|
|
|
CRDeclaration *
|
|
_st_theme_parse_declaration_list (const char *str)
|
|
{
|
|
return cr_declaration_parse_list_from_buf ((const guchar *)str,
|
|
CR_UTF_8);
|
|
}
|
|
|
|
/* Just g_warning for now until we have something nicer to do */
|
|
static CRStyleSheet *
|
|
parse_stylesheet_nofail (GFile *file)
|
|
{
|
|
GError *error = NULL;
|
|
CRStyleSheet *result;
|
|
|
|
result = parse_stylesheet (file, &error);
|
|
if (error)
|
|
{
|
|
g_warning ("%s", error->message);
|
|
g_clear_error (&error);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
insert_stylesheet (StTheme *theme,
|
|
GFile *file,
|
|
CRStyleSheet *stylesheet)
|
|
{
|
|
if (stylesheet == NULL)
|
|
return;
|
|
|
|
g_object_ref (file);
|
|
cr_stylesheet_ref (stylesheet);
|
|
|
|
g_hash_table_insert (theme->stylesheets_by_file, file, stylesheet);
|
|
g_hash_table_insert (theme->files_by_stylesheet, stylesheet, file);
|
|
}
|
|
|
|
/**
|
|
* st_theme_load_stylesheet:
|
|
* @theme: a #StTheme
|
|
* @file: a #GFile
|
|
* @error: (optional): a #GError
|
|
*
|
|
* Load the stylesheet associated with @file.
|
|
*
|
|
* Returns: %TRUE if successful
|
|
*/
|
|
gboolean
|
|
st_theme_load_stylesheet (StTheme *theme,
|
|
GFile *file,
|
|
GError **error)
|
|
{
|
|
CRStyleSheet *stylesheet;
|
|
|
|
stylesheet = parse_stylesheet (file, error);
|
|
if (!stylesheet)
|
|
return FALSE;
|
|
|
|
stylesheet->app_data = GUINT_TO_POINTER (TRUE);
|
|
|
|
insert_stylesheet (theme, file, stylesheet);
|
|
cr_stylesheet_ref (stylesheet);
|
|
theme->custom_stylesheets = g_slist_prepend (theme->custom_stylesheets, stylesheet);
|
|
g_signal_emit (theme, signals[STYLESHEETS_CHANGED], 0);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* st_theme_unload_stylesheet:
|
|
* @theme: a #StTheme
|
|
* @file: a #GFile
|
|
*
|
|
* Unload the stylesheet associated with @file. If @file was not loaded this
|
|
* function does nothing.
|
|
*/
|
|
void
|
|
st_theme_unload_stylesheet (StTheme *theme,
|
|
GFile *file)
|
|
{
|
|
CRStyleSheet *stylesheet;
|
|
|
|
stylesheet = g_hash_table_lookup (theme->stylesheets_by_file, file);
|
|
if (!stylesheet)
|
|
return;
|
|
|
|
if (!g_slist_find (theme->custom_stylesheets, stylesheet))
|
|
return;
|
|
|
|
theme->custom_stylesheets = g_slist_remove (theme->custom_stylesheets, stylesheet);
|
|
|
|
g_signal_emit (theme, signals[STYLESHEETS_CHANGED], 0);
|
|
|
|
/* We need to remove the entry from the hashtable after emitting the signal
|
|
* since we might still access the files_by_stylesheet hashtable in
|
|
* _st_theme_resolve_url() during the signal emission.
|
|
*/
|
|
g_hash_table_remove (theme->stylesheets_by_file, file);
|
|
g_hash_table_remove (theme->files_by_stylesheet, stylesheet);
|
|
cr_stylesheet_unref (stylesheet);
|
|
}
|
|
|
|
/**
|
|
* st_theme_get_custom_stylesheets:
|
|
* @theme: an #StTheme
|
|
*
|
|
* Get a list of the stylesheet files loaded with st_theme_load_stylesheet().
|
|
*
|
|
* Returns: (transfer full) (element-type GFile): the list of stylesheet files
|
|
* that were loaded with st_theme_load_stylesheet()
|
|
*/
|
|
GSList*
|
|
st_theme_get_custom_stylesheets (StTheme *theme)
|
|
{
|
|
GSList *result = NULL;
|
|
GSList *iter;
|
|
|
|
for (iter = theme->custom_stylesheets; iter; iter = iter->next)
|
|
{
|
|
CRStyleSheet *stylesheet = iter->data;
|
|
GFile *file = g_hash_table_lookup (theme->files_by_stylesheet, stylesheet);
|
|
|
|
result = g_slist_prepend (result, g_object_ref (file));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
st_theme_constructed (GObject *object)
|
|
{
|
|
StTheme *theme = ST_THEME (object);
|
|
CRStyleSheet *application_stylesheet;
|
|
CRStyleSheet *theme_stylesheet;
|
|
CRStyleSheet *default_stylesheet;
|
|
|
|
G_OBJECT_CLASS (st_theme_parent_class)->constructed (object);
|
|
|
|
application_stylesheet = parse_stylesheet_nofail (theme->application_stylesheet);
|
|
theme_stylesheet = parse_stylesheet_nofail (theme->theme_stylesheet);
|
|
default_stylesheet = parse_stylesheet_nofail (theme->default_stylesheet);
|
|
|
|
theme->cascade = cr_cascade_new (application_stylesheet,
|
|
theme_stylesheet,
|
|
default_stylesheet);
|
|
|
|
if (theme->cascade == NULL)
|
|
g_error ("Out of memory when creating cascade object");
|
|
|
|
insert_stylesheet (theme, theme->application_stylesheet, application_stylesheet);
|
|
insert_stylesheet (theme, theme->theme_stylesheet, theme_stylesheet);
|
|
insert_stylesheet (theme, theme->default_stylesheet, default_stylesheet);
|
|
}
|
|
|
|
static void
|
|
st_theme_finalize (GObject * object)
|
|
{
|
|
StTheme *theme = ST_THEME (object);
|
|
|
|
g_slist_foreach (theme->custom_stylesheets, (GFunc) cr_stylesheet_unref, NULL);
|
|
g_slist_free (theme->custom_stylesheets);
|
|
theme->custom_stylesheets = NULL;
|
|
|
|
g_hash_table_destroy (theme->stylesheets_by_file);
|
|
g_hash_table_destroy (theme->files_by_stylesheet);
|
|
|
|
g_clear_object (&theme->application_stylesheet);
|
|
g_clear_object (&theme->theme_stylesheet);
|
|
g_clear_object (&theme->default_stylesheet);
|
|
|
|
if (theme->cascade)
|
|
{
|
|
cr_cascade_unref (theme->cascade);
|
|
theme->cascade = NULL;
|
|
}
|
|
|
|
G_OBJECT_CLASS (st_theme_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
st_theme_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
StTheme *theme = ST_THEME (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_APPLICATION_STYLESHEET:
|
|
{
|
|
GFile *file = g_value_get_object (value);
|
|
|
|
if (!file_equal0 (file, theme->application_stylesheet))
|
|
{
|
|
g_clear_object (&theme->application_stylesheet);
|
|
if (file != NULL)
|
|
theme->application_stylesheet = g_object_ref (file);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case PROP_THEME_STYLESHEET:
|
|
{
|
|
GFile *file = g_value_get_object (value);
|
|
|
|
if (!file_equal0 (file, theme->theme_stylesheet))
|
|
{
|
|
g_clear_object (&theme->theme_stylesheet);
|
|
if (file != NULL)
|
|
theme->theme_stylesheet = g_object_ref (file);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case PROP_DEFAULT_STYLESHEET:
|
|
{
|
|
GFile *file = g_value_get_object (value);
|
|
|
|
if (!file_equal0 (file, theme->default_stylesheet))
|
|
{
|
|
g_clear_object (&theme->default_stylesheet);
|
|
if (file != NULL)
|
|
theme->default_stylesheet = g_object_ref (file);
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
st_theme_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
StTheme *theme = ST_THEME (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_APPLICATION_STYLESHEET:
|
|
g_value_set_object (value, theme->application_stylesheet);
|
|
break;
|
|
case PROP_THEME_STYLESHEET:
|
|
g_value_set_object (value, theme->theme_stylesheet);
|
|
break;
|
|
case PROP_DEFAULT_STYLESHEET:
|
|
g_value_set_object (value, theme->default_stylesheet);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* st_theme_new:
|
|
* @application_stylesheet: The highest priority stylesheet, representing application-specific
|
|
* styling; this is associated with the CSS "author" stylesheet, may be %NULL
|
|
* @theme_stylesheet: The second priority stylesheet, representing theme-specific styling ;
|
|
* this is associated with the CSS "user" stylesheet, may be %NULL
|
|
* @default_stylesheet: The lowest priority stylesheet, representing global default styling;
|
|
* this is associated with the CSS "user agent" stylesheet, may be %NULL
|
|
*
|
|
* Returns: the newly created theme object
|
|
**/
|
|
StTheme *
|
|
st_theme_new (GFile *application_stylesheet,
|
|
GFile *theme_stylesheet,
|
|
GFile *default_stylesheet)
|
|
{
|
|
StTheme *theme = g_object_new (ST_TYPE_THEME,
|
|
"application-stylesheet", application_stylesheet,
|
|
"theme-stylesheet", theme_stylesheet,
|
|
"default-stylesheet", default_stylesheet,
|
|
NULL);
|
|
|
|
return theme;
|
|
}
|
|
|
|
static gboolean
|
|
string_in_list (GString *stryng,
|
|
GStrv list)
|
|
{
|
|
gchar **it;
|
|
|
|
if (list == NULL)
|
|
return FALSE;
|
|
|
|
for (it = list; *it != NULL; it++)
|
|
{
|
|
if (!strqcmp (*it, stryng->str, stryng->len))
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
pseudo_class_add_sel_matches_style (StTheme *a_this,
|
|
CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
GStrv node_pseudo_classes;
|
|
|
|
g_return_val_if_fail (a_this
|
|
&& a_add_sel
|
|
&& a_add_sel->content.pseudo
|
|
&& a_add_sel->content.pseudo->name
|
|
&& a_add_sel->content.pseudo->name->stryng
|
|
&& a_add_sel->content.pseudo->name->stryng->str
|
|
&& a_node, FALSE);
|
|
|
|
node_pseudo_classes = st_theme_node_get_pseudo_classes (a_node);
|
|
|
|
return string_in_list (a_add_sel->content.pseudo->name->stryng,
|
|
node_pseudo_classes);
|
|
}
|
|
|
|
/**
|
|
* class_add_sel_matches_style:
|
|
* @a_add_sel: The class additional selector to consider.
|
|
* @a_node: The style node to consider.
|
|
*
|
|
* Returns: %TRUE if the class additional selector matches
|
|
* the style node given in argument, %FALSE otherwise.
|
|
*/
|
|
static gboolean
|
|
class_add_sel_matches_style (CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
GStrv element_classes;
|
|
|
|
g_return_val_if_fail (a_add_sel
|
|
&& a_add_sel->type == CLASS_ADD_SELECTOR
|
|
&& a_add_sel->content.class_name
|
|
&& a_add_sel->content.class_name->stryng
|
|
&& a_add_sel->content.class_name->stryng->str
|
|
&& a_node, FALSE);
|
|
|
|
element_classes = st_theme_node_get_element_classes (a_node);
|
|
|
|
return string_in_list (a_add_sel->content.class_name->stryng,
|
|
element_classes);
|
|
}
|
|
|
|
/*
|
|
*@return TRUE if the additional attribute selector matches
|
|
*the current style node given in argument, FALSE otherwise.
|
|
*@param a_add_sel the additional attribute selector to consider.
|
|
*@param a_node the style node to consider.
|
|
*/
|
|
static gboolean
|
|
id_add_sel_matches_style (CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
gboolean result = FALSE;
|
|
const char *id;
|
|
|
|
g_return_val_if_fail (a_add_sel
|
|
&& a_add_sel->type == ID_ADD_SELECTOR
|
|
&& a_add_sel->content.id_name
|
|
&& a_add_sel->content.id_name->stryng
|
|
&& a_add_sel->content.id_name->stryng->str
|
|
&& a_node, FALSE);
|
|
g_return_val_if_fail (a_add_sel
|
|
&& a_add_sel->type == ID_ADD_SELECTOR
|
|
&& a_node, FALSE);
|
|
|
|
id = st_theme_node_get_element_id (a_node);
|
|
|
|
if (id != NULL)
|
|
{
|
|
if (!strqcmp (id, a_add_sel->content.id_name->stryng->str,
|
|
a_add_sel->content.id_name->stryng->len))
|
|
{
|
|
result = TRUE;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
*additional_selector_matches_style:
|
|
*Evaluates if a given additional selector matches an style node.
|
|
*@param a_add_sel the additional selector to consider.
|
|
*@param a_node the style node to consider.
|
|
*@return TRUE is a_add_sel matches a_node, FALSE otherwise.
|
|
*/
|
|
static gboolean
|
|
additional_selector_matches_style (StTheme *a_this,
|
|
CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
CRAdditionalSel *cur_add_sel = NULL;
|
|
|
|
g_return_val_if_fail (a_add_sel, FALSE);
|
|
|
|
for (cur_add_sel = a_add_sel; cur_add_sel; cur_add_sel = cur_add_sel->next)
|
|
{
|
|
switch (cur_add_sel->type)
|
|
{
|
|
case NO_ADD_SELECTOR:
|
|
return FALSE;
|
|
case CLASS_ADD_SELECTOR:
|
|
if (!class_add_sel_matches_style (cur_add_sel, a_node))
|
|
return FALSE;
|
|
break;
|
|
case ID_ADD_SELECTOR:
|
|
if (!id_add_sel_matches_style (cur_add_sel, a_node))
|
|
return FALSE;
|
|
break;
|
|
case ATTRIBUTE_ADD_SELECTOR:
|
|
g_warning ("Attribute selectors not supported");
|
|
return FALSE;
|
|
case PSEUDO_CLASS_ADD_SELECTOR:
|
|
if (!pseudo_class_add_sel_matches_style (a_this, cur_add_sel, a_node))
|
|
return FALSE;
|
|
break;
|
|
default:
|
|
g_warning ("Unhandled selector type %d", cur_add_sel->type);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
element_name_matches_type (const char *element_name,
|
|
GType element_type)
|
|
{
|
|
if (element_type == G_TYPE_NONE)
|
|
{
|
|
return strcmp (element_name, "stage") == 0;
|
|
}
|
|
else
|
|
{
|
|
GType match_type = g_type_from_name (element_name);
|
|
if (match_type == G_TYPE_INVALID)
|
|
return FALSE;
|
|
|
|
return g_type_is_a (element_type, match_type);
|
|
}
|
|
}
|
|
|
|
/*
|
|
*Evaluate a selector (a simple selectors list) and says
|
|
*if it matches the style node given in parameter.
|
|
*The algorithm used here is the following:
|
|
*Walk the combinator separated list of simple selectors backward, starting
|
|
*from the end of the list. For each simple selector, looks if
|
|
*if matches the current style.
|
|
*
|
|
*@param a_this the selection engine.
|
|
*@param a_sel the simple selection list.
|
|
*@param a_node the style node.
|
|
*@param a_result out parameter. Set to true if the
|
|
*selector matches the style node, FALSE otherwise.
|
|
*@param a_recurse if set to TRUE, the function will walk to
|
|
*the next simple selector (after the evaluation of the current one)
|
|
*and recursively evaluate it. Must be usually set to TRUE unless you
|
|
*know what you are doing.
|
|
*/
|
|
static enum CRStatus
|
|
sel_matches_style_real (StTheme *a_this,
|
|
CRSimpleSel *a_sel,
|
|
StThemeNode *a_node,
|
|
gboolean *a_result,
|
|
gboolean a_eval_sel_list_from_end,
|
|
gboolean a_recurse)
|
|
{
|
|
CRSimpleSel *cur_sel = NULL;
|
|
StThemeNode *cur_node = NULL;
|
|
GType cur_type;
|
|
|
|
*a_result = FALSE;
|
|
|
|
if (a_eval_sel_list_from_end)
|
|
{
|
|
/*go and get the last simple selector of the list */
|
|
for (cur_sel = a_sel; cur_sel && cur_sel->next; cur_sel = cur_sel->next)
|
|
;
|
|
}
|
|
else
|
|
{
|
|
cur_sel = a_sel;
|
|
}
|
|
|
|
cur_node = a_node;
|
|
cur_type = st_theme_node_get_element_type (cur_node);
|
|
|
|
while (cur_sel)
|
|
{
|
|
if (((cur_sel->type_mask & TYPE_SELECTOR)
|
|
&& (cur_sel->name
|
|
&& cur_sel->name->stryng
|
|
&& cur_sel->name->stryng->str)
|
|
&&
|
|
(element_name_matches_type (cur_sel->name->stryng->str, cur_type)))
|
|
|| (cur_sel->type_mask & UNIVERSAL_SELECTOR))
|
|
{
|
|
/*
|
|
*this simple selector
|
|
*matches the current style node
|
|
*Let's see if the preceding
|
|
*simple selectors also match
|
|
*their style node counterpart.
|
|
*/
|
|
if (cur_sel->add_sel)
|
|
{
|
|
if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node))
|
|
goto walk_a_step_in_expr;
|
|
else
|
|
goto done;
|
|
}
|
|
else
|
|
goto walk_a_step_in_expr;
|
|
}
|
|
if (!(cur_sel->type_mask & TYPE_SELECTOR)
|
|
&& !(cur_sel->type_mask & UNIVERSAL_SELECTOR))
|
|
{
|
|
if (!cur_sel->add_sel)
|
|
goto done;
|
|
if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node))
|
|
goto walk_a_step_in_expr;
|
|
else
|
|
goto done;
|
|
}
|
|
else
|
|
{
|
|
goto done;
|
|
}
|
|
|
|
walk_a_step_in_expr:
|
|
if (a_recurse == FALSE)
|
|
{
|
|
*a_result = TRUE;
|
|
goto done;
|
|
}
|
|
|
|
/*
|
|
*here, depending on the combinator of cur_sel
|
|
*choose the axis of the element tree traversal
|
|
*and walk one step in the element tree.
|
|
*/
|
|
if (!cur_sel->prev)
|
|
break;
|
|
|
|
switch (cur_sel->combinator)
|
|
{
|
|
case NO_COMBINATOR:
|
|
break;
|
|
|
|
case COMB_WS: /*descendant selector */
|
|
{
|
|
StThemeNode *n = NULL;
|
|
|
|
/*
|
|
*walk the element tree upward looking for a parent
|
|
*style that matches the preceding selector.
|
|
*/
|
|
for (n = st_theme_node_get_parent (a_node); n; n = st_theme_node_get_parent (n))
|
|
{
|
|
enum CRStatus status;
|
|
gboolean matches = FALSE;
|
|
|
|
status = sel_matches_style_real (a_this, cur_sel->prev, n, &matches, FALSE, TRUE);
|
|
|
|
if (status != CR_OK)
|
|
goto done;
|
|
|
|
if (matches)
|
|
{
|
|
cur_node = n;
|
|
cur_type = st_theme_node_get_element_type (cur_node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!n)
|
|
{
|
|
/*
|
|
*didn't find any ancestor that matches
|
|
*the previous simple selector.
|
|
*/
|
|
goto done;
|
|
}
|
|
/*
|
|
*in this case, the preceding simple sel
|
|
*will have been interpreted twice, which
|
|
*is a cpu and mem waste ... I need to find
|
|
*another way to do this. Anyway, this is
|
|
*my first attempt to write this function and
|
|
*I am a bit clueless.
|
|
*/
|
|
break;
|
|
}
|
|
|
|
case COMB_PLUS:
|
|
g_warning ("+ combinators are not supported");
|
|
goto done;
|
|
|
|
case COMB_GT:
|
|
cur_node = st_theme_node_get_parent (cur_node);
|
|
if (!cur_node)
|
|
goto done;
|
|
cur_type = st_theme_node_get_element_type (cur_node);
|
|
break;
|
|
|
|
default:
|
|
goto done;
|
|
}
|
|
|
|
cur_sel = cur_sel->prev;
|
|
}
|
|
|
|
/*
|
|
*if we reached this point, it means the selector matches
|
|
*the style node.
|
|
*/
|
|
*a_result = TRUE;
|
|
|
|
done:
|
|
return CR_OK;
|
|
}
|
|
|
|
static void
|
|
add_matched_properties (StTheme *a_this,
|
|
CRStyleSheet *a_nodesheet,
|
|
StThemeNode *a_node,
|
|
GPtrArray *props)
|
|
{
|
|
CRStatement *cur_stmt = NULL;
|
|
CRSelector *sel_list = NULL;
|
|
CRSelector *cur_sel = NULL;
|
|
gboolean matches = FALSE;
|
|
enum CRStatus status = CR_OK;
|
|
|
|
/*
|
|
*walk through the list of statements and,
|
|
*get the selectors list inside the statements that
|
|
*contain some, and try to match our style node in these
|
|
*selectors lists.
|
|
*/
|
|
for (cur_stmt = a_nodesheet->statements; cur_stmt; cur_stmt = cur_stmt->next)
|
|
{
|
|
/*
|
|
*initialyze the selector list in which we will
|
|
*really perform the search.
|
|
*/
|
|
sel_list = NULL;
|
|
|
|
/*
|
|
*get the the damn selector list in
|
|
*which we have to look
|
|
*/
|
|
switch (cur_stmt->type)
|
|
{
|
|
case RULESET_STMT:
|
|
if (cur_stmt->kind.ruleset && cur_stmt->kind.ruleset->sel_list)
|
|
{
|
|
sel_list = cur_stmt->kind.ruleset->sel_list;
|
|
}
|
|
break;
|
|
|
|
case AT_IMPORT_RULE_STMT:
|
|
{
|
|
CRAtImportRule *import_rule = cur_stmt->kind.import_rule;
|
|
|
|
if (import_rule->sheet == NULL)
|
|
{
|
|
GFile *file = NULL;
|
|
|
|
if (import_rule->url->stryng && import_rule->url->stryng->str)
|
|
{
|
|
file = _st_theme_resolve_url (a_this,
|
|
a_nodesheet,
|
|
import_rule->url->stryng->str);
|
|
import_rule->sheet = parse_stylesheet (file, NULL);
|
|
}
|
|
|
|
if (import_rule->sheet)
|
|
{
|
|
insert_stylesheet (a_this, file, import_rule->sheet);
|
|
/* refcount of stylesheets starts off at zero, so we don't need to unref! */
|
|
}
|
|
else
|
|
{
|
|
/* Set a marker to avoid repeatedly trying to parse a non-existent or
|
|
* broken stylesheet
|
|
*/
|
|
import_rule->sheet = (CRStyleSheet *) - 1;
|
|
}
|
|
|
|
if (file)
|
|
g_object_unref (file);
|
|
}
|
|
|
|
if (import_rule->sheet != (CRStyleSheet *) - 1)
|
|
{
|
|
add_matched_properties (a_this, import_rule->sheet,
|
|
a_node, props);
|
|
}
|
|
}
|
|
break;
|
|
case AT_MEDIA_RULE_STMT:
|
|
case AT_RULE_STMT:
|
|
case AT_PAGE_RULE_STMT:
|
|
case AT_CHARSET_RULE_STMT:
|
|
case AT_FONT_FACE_RULE_STMT:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!sel_list)
|
|
continue;
|
|
|
|
/*
|
|
*now, we have a comma separated selector list to look in.
|
|
*let's walk it and try to match the style node
|
|
*on each item of the list.
|
|
*/
|
|
for (cur_sel = sel_list; cur_sel; cur_sel = cur_sel->next)
|
|
{
|
|
if (!cur_sel->simple_sel)
|
|
continue;
|
|
|
|
status = sel_matches_style_real (a_this, cur_sel->simple_sel, a_node, &matches, TRUE, TRUE);
|
|
|
|
if (status == CR_OK && matches)
|
|
{
|
|
CRDeclaration *cur_decl = NULL;
|
|
|
|
/* In order to sort the matching properties, we need to compute the
|
|
* specificity of the selector that actually matched this
|
|
* element. In a non-thread-safe fashion, we store it in the
|
|
* ruleset. (Fixing this would mean cut-and-pasting
|
|
* cr_simple_sel_compute_specificity(), and have no need for
|
|
* thread-safety anyways.)
|
|
*
|
|
* Once we've sorted the properties, the specificity no longer
|
|
* matters and it can be safely overriden.
|
|
*/
|
|
cr_simple_sel_compute_specificity (cur_sel->simple_sel);
|
|
|
|
cur_stmt->specificity = cur_sel->simple_sel->specificity;
|
|
|
|
for (cur_decl = cur_stmt->kind.ruleset->decl_list; cur_decl; cur_decl = cur_decl->next)
|
|
g_ptr_array_add (props, cur_decl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#define ORIGIN_OFFSET_IMPORTANT (NB_ORIGINS)
|
|
#define ORIGIN_OFFSET_EXTENSION (NB_ORIGINS * 2)
|
|
|
|
static inline int
|
|
get_origin (const CRDeclaration * decl)
|
|
{
|
|
enum CRStyleOrigin origin = decl->parent_statement->parent_sheet->origin;
|
|
gboolean is_extension_sheet = GPOINTER_TO_UINT (decl->parent_statement->parent_sheet->app_data);
|
|
|
|
if (decl->important)
|
|
origin += ORIGIN_OFFSET_IMPORTANT;
|
|
|
|
if (is_extension_sheet)
|
|
origin += ORIGIN_OFFSET_EXTENSION;
|
|
|
|
return origin;
|
|
}
|
|
|
|
/* Order of comparison is so that higher priority statements compare after
|
|
* lower priority statements */
|
|
static int
|
|
compare_declarations (gconstpointer a,
|
|
gconstpointer b)
|
|
{
|
|
/* g_ptr_array_sort() is broooken */
|
|
CRDeclaration *decl_a = *(CRDeclaration **) a;
|
|
CRDeclaration *decl_b = *(CRDeclaration **) b;
|
|
|
|
int origin_a = get_origin (decl_a);
|
|
int origin_b = get_origin (decl_b);
|
|
|
|
if (origin_a != origin_b)
|
|
return origin_a - origin_b;
|
|
|
|
if (decl_a->parent_statement->specificity != decl_b->parent_statement->specificity)
|
|
return decl_a->parent_statement->specificity - decl_b->parent_statement->specificity;
|
|
|
|
return 0;
|
|
}
|
|
|
|
GPtrArray *
|
|
_st_theme_get_matched_properties (StTheme *theme,
|
|
StThemeNode *node)
|
|
{
|
|
enum CRStyleOrigin origin = 0;
|
|
CRStyleSheet *sheet = NULL;
|
|
GPtrArray *props = g_ptr_array_new ();
|
|
GSList *iter;
|
|
|
|
g_return_val_if_fail (ST_IS_THEME (theme), NULL);
|
|
g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL);
|
|
|
|
for (origin = ORIGIN_UA; origin < NB_ORIGINS; origin++)
|
|
{
|
|
sheet = cr_cascade_get_sheet (theme->cascade, origin);
|
|
if (!sheet)
|
|
continue;
|
|
|
|
add_matched_properties (theme, sheet, node, props);
|
|
}
|
|
|
|
for (iter = theme->custom_stylesheets; iter; iter = iter->next)
|
|
add_matched_properties (theme, iter->data, node, props);
|
|
|
|
/* We count on a stable sort here so that later declarations come
|
|
* after earlier declarations */
|
|
g_ptr_array_sort (props, compare_declarations);
|
|
|
|
return props;
|
|
}
|
|
|
|
/* Resolve an url from an url() reference in a stylesheet into a GFile,
|
|
* if possible. The resolution here is distinctly lame and
|
|
* will fail on many examples.
|
|
*/
|
|
GFile *
|
|
_st_theme_resolve_url (StTheme *theme,
|
|
CRStyleSheet *base_stylesheet,
|
|
const char *url)
|
|
{
|
|
char *scheme;
|
|
GFile *resource;
|
|
|
|
if ((scheme = g_uri_parse_scheme (url)))
|
|
{
|
|
g_free (scheme);
|
|
resource = g_file_new_for_uri (url);
|
|
}
|
|
else if (base_stylesheet != NULL)
|
|
{
|
|
GFile *base_file = NULL, *parent;
|
|
|
|
base_file = g_hash_table_lookup (theme->files_by_stylesheet, base_stylesheet);
|
|
|
|
/* This is an internal function, if we get here with
|
|
a bad @base_stylesheet we have a problem. */
|
|
g_assert (base_file);
|
|
|
|
parent = g_file_get_parent (base_file);
|
|
resource = g_file_resolve_relative_path (parent, url);
|
|
|
|
g_object_unref (parent);
|
|
}
|
|
else
|
|
{
|
|
resource = g_file_new_for_path (url);
|
|
}
|
|
|
|
return resource;
|
|
}
|