Beating up on gtk+

So the biggest thing I have learned doing all of this is how to somewhat reasonably make changes to gtk+. Here are some of the lessons I've been through, but you don't have to take MY word for it. Or do you?


gtk+'s Makefile system

Going into this I knew about thing 2 on the list of a million things to know about Makefiles, automake, and autoconf. Through the course of adding key tables to gtk+ I learned some important things.

Generic Lesson One: Look for Things that are Similar to What You are Doing that have Already Been Done!

This is the only thing that will save you. It is the only thing that saved me throughout this process. Now here are some specifics...

Getting your files in the distro

One of my first problems of getting all this integrated was getting my new code compiled into the actual gdk/gtk library. Editing existing files made changes to those files just fine, but I had to figure out a way to make my new junk get linked into the main library.

To solve this problem, I dug through the Makefiles. Makefiles that are automatically generated are not very nice. I played with them for some time before giving up on the actual end-product Makefile for ever getting anything into gtk+.

My next hope was to take one step down the food chain to the Makefile.am files. These are files that are parsed by automake to generate the end-product makefiles I was suffering with. Here is where I hit gold.

The Makefile.am in both the gdk/ and gtk/ directories under the main source tree hold exactly the information you need to get your code linked in and your header files stuck in the right include directories.

For gtk:

important file: gtk/Makefile.am

Header files can be slipped right into the gtk_public_h_sources shell variable. Each .h file for the gtk portion of gtk+ that is to be stuck in the include directory appears here, in alphabetical order:

# GTK+ header files for public installation (non-generated)
gtk_public_h_sources = @STRIP_BEGIN@ \
        gtk.h                   \
        gtkaccelgroup.h         \
        gtkaccellabel.h         \
        gtkadjustment.h         \
        gtkalignment.h          \
        gtkarg.h                \
        gtkarrow.h              \
        gtkaspectframe.h        \
        gtkbin.h                \
	...etc...

Code that should end up in the gtk library must be put in the gtk_c_sources shell var. Again, this is just a big list of files in alphabetical order:

# GTK+ C sources to build the library from
gtk_c_sources = @STRIP_BEGIN@ \
        gtkaccelgroup.c         \
        gtkaccellabel.c         \
        gtkadjustment.c         \
        gtkalignment.c          \
        gtkarg.c                \
        gtkarrow.c              \
        gtkaspectframe.c        \
        gtkbin.c                \
	... etc...

For gdk:

important file: gdk/Makefile.am

Like the gtk side, include files for the gdk side of the library go in gdk_public_h_sources, in alphabetical order:

# GDK header files for public installation (non-generated)
gdk_public_h_sources = @STRIP_BEGIN@ \
        gdk.h           \
        gdkcursors.h    \
        gdkrgb.h        \
        gdki18n.h       \
        gdkkeysyms.h    \
        gdkkeytable.h   \
        gdkprivate.h    \
	... etc...

and files destined for the library go in gdk_c_sources:

gdk_c_sources = @STRIP_BEGIN@ \
        gdk.c           \
        gdkcc.c         \
        gdkcolor.c      \
        gdkcursor.c     \
        gdkdnd.c        \
        gdkdraw.c       \
        gdkevents.c     \
	... etc...

Just be sure you escape the newline between each 'line' of the list of files.

But beware!!! You are not done!!!! Now you must go to the base directory of the gtk+ source tree and type:

$automake

Only then will the Makefiles be regenerated containing all of your new and happy code for compilation and installation.


Configuring through ~/.gtkrc and its ilk

You poor kid.

Remembering that all I know about is gtk+-1.2.10 (and not even all that much about it), this situation may be entirely different by gtk1.3/2.0 stabilization time. I have heard about GConf being an easy way to do config files or whatnot in the future... but for right now...

WARNING: gtkrc and all of its related code are meant exclusively (i.e. - only, with nothing else in mind, for no purpose other than) for styles!!! All that the parser recognizes by default are some simple commands like 'include' (though I can't find the syntax on that, lately), 'style', 'bgcolor', etc. To want to include your own configuration options in a .gtkrc file is madness.

Welcome, brother (or sister). :D

So the first step to success is to come up with some kind of simple grammar that you want to use as a pattern for your configuration. Again:

Look for Things Like You Want That Have Already Been Done

After some playing, I decided that I would use configs in the form:

	keytable	"name of table"
	{
		"name of action" = "key combination"[, "another key"[, "another key"]]
		... etc...
	}

So what did I need to get the parser to recognize something of this form? The only new token I had really added was the special word 'keytable'. Everything else I could fake as a string (and '=', ',', '{', and '}' are already recognized by the tokenizer gtkrc uses). So now what?

I opened up gtk/gtkrc.h and found the definition of the 'GtkRcTokenType' enumerated type, and I slipped my new token in it:

	typedef enum {
	  GTK_RC_TOKEN_INVALID = G_TOKEN_LAST,	/* <-- put nothing before this */
	  GTK_RC_TOKEN_INCLUDE,
	  GTK_RC_TOKEN_NORMAL,
	  GTK_RC_TOKEN_ACTIVE,
	  GTK_RC_TOKEN_PRELIGHT,
	  GTK_RC_TOKEN_SELECTED,
	  GTK_RC_TOKEN_INSENSITIVE,
	  GTK_RC_TOKEN_FG,
	  GTK_RC_TOKEN_BG,
	  GTK_RC_TOKEN_BASE,
	  GTK_RC_TOKEN_TEXT,
	  GTK_RC_TOKEN_FONT,
	  GTK_RC_TOKEN_FONTSET,
	  GTK_RC_TOKEN_BG_PIXMAP,
	  GTK_RC_TOKEN_PIXMAP_PATH,
	  GTK_RC_TOKEN_STYLE,
	  GTK_RC_TOKEN_BINDING,
	  GTK_RC_TOKEN_BIND,
	  GTK_RC_TOKEN_WIDGET,
	  GTK_RC_TOKEN_WIDGET_CLASS,
	  GTK_RC_TOKEN_CLASS,
	  GTK_RC_TOKEN_LOWEST,
	  GTK_RC_TOKEN_GTK,
	  GTK_RC_TOKEN_APPLICATION,
	  GTK_RC_TOKEN_RC,
	  GTK_RC_TOKEN_HIGHEST,
	  GTK_RC_TOKEN_ENGINE,
	  GTK_RC_TOKEN_MODULE_PATH,
	  GTK_RC_TOKEN_KEYTABLE,	/* <-- my new token! */
	  GTK_RC_TOKEN_LAST		/* do NOT put anything after this */
	} GtkRcTokenType;

I then had to go into gtk/gtkrc.c and define the string to match that token in the symbols table:

	static const struct
	{
	  gchar *name;
	  guint token;
	} symbols[] = {
	  { "include", GTK_RC_TOKEN_INCLUDE },
	  { "NORMAL", GTK_RC_TOKEN_NORMAL },
	  { "ACTIVE", GTK_RC_TOKEN_ACTIVE },
	  { "PRELIGHT", GTK_RC_TOKEN_PRELIGHT },
	  { "SELECTED", GTK_RC_TOKEN_SELECTED },
	  { "INSENSITIVE", GTK_RC_TOKEN_INSENSITIVE },
	  { "fg", GTK_RC_TOKEN_FG },
	  { "bg", GTK_RC_TOKEN_BG },
	  { "base", GTK_RC_TOKEN_BASE },
	  { "text", GTK_RC_TOKEN_TEXT },
	  { "font", GTK_RC_TOKEN_FONT },
	  { "fontset", GTK_RC_TOKEN_FONTSET },
	  { "bg_pixmap", GTK_RC_TOKEN_BG_PIXMAP },
	  { "pixmap_path", GTK_RC_TOKEN_PIXMAP_PATH },
	  { "style", GTK_RC_TOKEN_STYLE },
	  { "binding", GTK_RC_TOKEN_BINDING },
	  { "bind", GTK_RC_TOKEN_BIND },
	  { "widget", GTK_RC_TOKEN_WIDGET },
	  { "widget_class", GTK_RC_TOKEN_WIDGET_CLASS },
	  { "class", GTK_RC_TOKEN_CLASS },
	  { "lowest", GTK_RC_TOKEN_LOWEST },
	  { "gtk", GTK_RC_TOKEN_GTK },
	  { "application", GTK_RC_TOKEN_APPLICATION },
	  { "rc", GTK_RC_TOKEN_RC },
	  { "highest", GTK_RC_TOKEN_HIGHEST },
	  { "engine", GTK_RC_TOKEN_ENGINE },
	  { "module_path", GTK_RC_TOKEN_MODULE_PATH },
	  { "keytable", GTK_RC_TOKEN_KEYTABLE },
	};

Now what? The tokenizer has the power to recognize my new keyword and all the other things in quotes as strings, but what does it do when it finds 'keytable' sitting there in its stream of tokens?

Well, after much digging, I found that as soon as the scanner starts, it calls gtk_rc_parse_statment. gtk_rc_parse_statement, in turn, looks at the first token in the stream and, assuming that token is for a valid keyword to start a config statement, it calls an appropriate gtk_parse_[descriptive name here] function, passing it the scanner. Since my keyword 'keytable' starts off just such a statement, I included it in their switch statement, with a call to a function I had yet to write: gtk_rc_parse_keytable:

	gtk_rc_parse_statement (GScanner *scanner)
	{
	  guint token;
	  
	  token = g_scanner_peek_next_token (scanner);

	  switch (token)
	    {
		... stuff...
	    case GTK_RC_TOKEN_WIDGET_CLASS:
	      return gtk_rc_parse_path_pattern (scanner);

	    case GTK_RC_TOKEN_CLASS:
	      return gtk_rc_parse_path_pattern (scanner);

	    case GTK_RC_TOKEN_MODULE_PATH:
	      return gtk_rc_parse_module_path (scanner);
	       
	    case GTK_RC_TOKEN_KEYTABLE:
	      return gtk_rc_parse_keytable (scanner);
		... etc...

And once again it was time to resort to my now-irritating-to-the-reader credo:

Look For Stuff That Has Already Been Done That Will Do What You Want
(like how I keep rewording it?)

What was my gtk_rc_parse_keytable function supposed to do? I guess somehow start taking a keytable configuration block from the incoming rc file, but HOW??

So I ripped off a bunch of their stuff and started breaking the parse up into smaller and smaller subfunctions, slowly building a keytable local to gtkrc to be inserted into its hash of keytables. In the end I had gtk_rc_parse_keytable, gtk_rc_parse_keyentry, gtk_rc_parse_keykey. Here is their semi-correct order of operations:

  1. gtk_rc_parse_keytable checks to see if the first token is really "keytable". If it is, the parse continues, looking for a string as the next token. Whatever that string is, a new keytable is made, and is given that string as its name. It then checks to make sure '{' is the next token in the stream. If none of these things fail and bomb it out, it calls...
  2. gtk_rc_parse_keyentry, which grabs the next token, expecting it to be a string. This string becomes the name for a new entry in the keytable. Since gtkrc doesn't have access to any enumeration or any list of IDs for entries, the ID field is pretty much useless in the context of gtkrc, it just matches the string for the GdkKeyEntry. After that string is made the name of the entry, it checks that '=' is the next token, and that another string is found. If there really is another string after the '=', it calls...
  3. gtk_rc_parse_keykey which is another parsing function altogether. It throws the contents of the string at gtk's key accelerator code for parsing a string into a key and a keyboard mask. This set of values is set as the first key combination for the entry. If successful, it jumps back to...
  4. gtk_rc_parse_keyentry, where code can now branch:
    1. If the next token is ',', gtk_rc_parse_keykey is called again, and the new key is added to the entry being parsed, control then returns to the start of this branch.
    2. If the next token is NOT a ',', control gtk_rc_parse_keyentry ends, returning control to gtk_rc_parse_keytable.
  5. if gtk_rc_parse_keytable now sees a '}' as the next token, it is done. Otherwise it assumes it is looking at another key entry and begins that process again.
  6. When this is all truly done for a particular keytable, the table is added to a hash internal to gtkrc. This is just the way I did it, you can do whatever you like, but this is also the way they store their styles, in an internal hash to gtkrc.

Okay, that was a terrible execution tree, but maybe you get the idea. Steal the other parsing code to do what you want.

Soooo that talk of hashes brings me to my next point. Now that I've parsed all this JUNK of mine, how do I get to it? How do I access it?!!??!?! GAAAHHH!!!!!

Well, it's fairly simple. Just ask gtkrc to fix things for you. I wrote a funtion in gtkrc.c called 'gtk_rc_keytable_set_from_config' (yes, a terrible bad name). It takes in a key table, looks for one with a matching name in its local hash, and replaces all keys for all entries it can find with matching strings (ID numbers are preserved, entries in the hash with no counterpart in the incoming table are ignored).

C'EST MAGIQUE!!! Now you can put your configs for whatever in any of the many gtkrc config files scattered about your themes and home directories. Hunh!

And, um, that's it. It's fairly ugly, and I hear that gtk+ 2.0 will have many nice fixes to config file making, but I don't know them yet, so this is all I have for you. Hope it made sense. :D