Mathias Hasselmann

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.

Comments

Benjamin Berg commented on September 22, 2009 at 12:16 p.m.

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.

Mathias Hasselmann commented on September 22, 2009 at 1:16 p.m.

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.

Cole commented on September 28, 2009 at 1:53 p.m.

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.

Cole commented on September 28, 2009 at 2:01 p.m.

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>