Application Theming Tricks
From time to time applications need custom theming rules. Especially when the project has professional UI designers involved. So how to achieve this with GTK+?
Trivial Theming
Most easy and very wrong:
if (gdk_color_parse ("pink", &color))
gtk_widget_modify_bg (widget, GTK_STATE_NORMAL, &color);
This will break and look childish as soon as your users use a custom color scheme.
Better:
static void
style_set_cb (GtkWidget *widget,
GtkStyle *old_style)
{
GtkStyle *style = gtk_widget_get_style (widget);
if (gtk_style_lookup_color (style, "SecondaryTextColor", &color))
gtk_widget_modify_bg (widget, GTK_STATE_NORMAL, &color);
}
static void
my_widget_init (MyWidget *widget)
{
g_signal_connect (widget, "style-set", G_CALLBACK (style_set_cb), NULL);
}
This will allow theme designers to override your color choice. Notice that you'll have to update those color overrides when the theme changes. Btw, the "style-set" signal is emitted when the widget is realized, therefore you don't have to manually invoke the callback during widget construction.
Guess it's also worth to mention that Hildon provides convenience API for simple theming requirements.
Complex Problems
So for simple requirements GTK+ (and Hildon) have reasonable API support. Things become troublesome when your designers invent rules like "this widget has a rounded border and drop shadow, but only within buttons". Obviously border and drop shadow radius should be themeable and therefore are implemented as style properties, but how to impose this rule?
You could scan the widget hierarchy when choosing default values for your style properties:
button = gtk_widget_get_ancestor (widget, GTK_TYPE_BUTTON);
gtk_widget_style_get (widget, "border-radius", &border_radius, NULL);
if (button)
border_radius = (button ? 12 : 0);
You'll quickly notice the flawed hard coded default value. Also such things are hard to override in theme files. So it's probably better to apply a custom theming rule via gtk_rc_parse_string():
static void
my_widget_class_init (MyWidgetClass *class)
{
...
gtk_rc_parse_string
("style 'my-widget-style-clickable' {"
" MyWidget::border-radius = 2"
"}"
"widget_class '*.<GtkButton>.MyWidget'"
"style 'my-widget-style-clickable''");
...
}
Application Theme Files
Looks like a perfect solution, until you realize that this rule is applied after all rules loaded from gtkrc files!
So how to inject this rule before the user's theming rules? This was a big question to me until I've found gtk_rc_add_default_file(). Well almost: This function only adds files to the end of the search path. Therefore it suffers from the same issues as gtk_rc_parse_string(). Fortunately the API author was smart enough to also provide gtk_rc_get_default_files() and gtk_rc_set_default_files(). Those functions can be used to apply application specific theming rules, which can be overwritten by the user - drum roll please:
static void
inject_rc_file (const char *filename)
{
char **system_rc_files, **p;
GPtrArray *custom_rc_files;
system_rc_files = gtk_rc_get_default_files ();
custom_rc_files = g_ptr_array_new ();
g_ptr_array_add (custom_rc_files, g_strdup (filename));
for (p = system_rc_files; *p; ++p)
g_ptr_array_add (custom_rc_files, g_strdup (*p));
g_ptr_array_add (custom_rc_files, NULL);
gtk_rc_set_default_files ((gpointer) custom_rc_files->pdata);
g_strfreev ((gpointer) g_ptr_array_free (custom_rc_files, FALSE));
}
int
main (int argc,
char **argv)
{
...
inject_rc_file (PKGDATADIR "/gtkrc." PACKAGE);
gtk_init (&argc, &argv);
...
}
Update: Benjamin Berg just pointed out that priorities can be assigned to styles. So the following should work fine:
gtk_rc_parse_string
("style 'my-widget-style-clickable' {"
" MyWidget::border-radius = 2"
"}"
"widget_class '*.<GtkButton>.MyWidget'"
"style : lowest 'my-widget-style-clickable''");
Awesome, little know feature.
You can change the priority even in gtk_rc_parse_string. By dropping it to eg. :lowest, it will only be used if there is nothing else in the theme or the users .gtkrc-2.0 (see also http://live.gnome.org/GnomeArt/Tutori...).
So using the following should work fine:
gtk_rc_parse_string
("style 'my-widget-style-clickable' {"
" MyWidget::border-radius = 2"
"}"
"widget_class '*.<GtkButton>.MyWidget'"
"style : lowest 'my-widget-style-clickable''");
One should also be careful when changing some of the widgets colors. These changes may not work together with the themes colors. An example for this are font colors which need to have enough contrast on the themes background.
Benjamin: Oh. Awesome! Guess GTK+ docs and the wiki should be updated to promote that feature! It's a quite essential feature for custom widgets.
Unfortunately custom widget theming is very dependant on the engine your using; to whether it actually works or not.
A lot of engines use macros like GTK_IS_XXX to decide what widget is being rendered; some just assert doing an invalid cast.
Not so good for a custom widget not deriving directly a widget it's trying to look like.
I wrap any custom widget rendering I do in a block of code that either renders my representation of the widget or the current engines.
At least this way a user is not forced into using an engine just so an app with custom widgets is rendered/works correctly.
Sorry it should read as...
<snip>
Not so good for a custom widget not deriving directly from a widget it's trying to look like.
</snip>