Writing a Bonobo Control

Controls are a kind of widgets - so what makes them so special? Well, controls also allow for discovery of their capabilities at runtime. Also, you don't necessarily need source code (or even header files) to use a control. The runtime discovery stuff is also very handy for GUI-builders (such as Glade); surely you can't expect the Glade maintainers to write custom code for every widget written; the use of Controls allows for a generic approach. This also makes it a lot easier to use controls from other languages, such as Perl or Python. Finally, using a component model allows the running of your controls on other machines. There must be someone, somewhere who wants that.

A custom widget

The easiest way to write a control is by packing a GTK+-widget; you can take an existing widget, you can combine some existing widgets (e.g. by putting them together in a GTKHBox), or you can write a custom widget. Of course, this depends on the kind of thing you want to do with your controls. In this tutorial, we use the example of a custom GTK+-widget, GtkIPEntry, a very simple widget to enter IP numbers. In fact, it's so simple it's not very interesting for anything but this tutorial. Our GtkIPEntry has four public functions:

Also, the GtkIPEntry exposes one signal: the "changed" signal is emitted when any of the numbers in the GtkIPEntry changes.

Ok, very easy, right? You can find the source code for this widget in gtkipentry.h and gtkipentry.c. Admittedly, the control needs a lot of work to become actually useful, but that's not the point here ;-)

Turning it into a control

Ok, now comes the interesting stuff: how to turn this thing into a control? The short answer is that we need to write a control factory[1], i.e., we need to write a program which will 'produce' a new control on request. You can see the code in ipctrl.c and ipctrl.h.

As stated, we need to write a factory that will produce a control (let's call it a IPCtrl). Here comes the code (there's more in ipctrl.c, but we will add stuff gradually):
	BonoboControl* control;
	GtkWidget*     ipentry;

	ipentry = gtk_ip_entry_new ();
        gtk_widget_show (ipentry);

        /* create a BonoboControl from a widget */
        control = bonobo_control_new (ipentry);
	
This is all that's needed for a minimal control (the code is paraphrased from bonobo_ipctrl_factory in ipctrl.c). [1]

Adding a property bag

A control wouldn't be very useful if it couldn't communicate with the application it's in, right? In our example, the application probably would like to know what IP number was entered, or fill in the IP number. In Bonobo, the way to do this is by using property bags.

A property bag is a Bonobo object maintaining a list of named and typed parameters. Note that the concept of a property bag goes very nicely with the Bonobo default implementations of CORBA interfaces, as we do not need to extend the IDL interfaces to add properties. So property bags save us a lot of work.

In our control, we create such a property bag. We must define two accessor functions (in this case: get_prop and set_prop), to allow clients to communicate with our property bag. Again, from ipctrl.c:
/* 
 * get the value from one of the gtkipentry octets
 */ 
static void
get_prop (BonoboPropertyBag *bag,
	  BonoboArg *arg,
	  guint arg_id,
	  CORBA_Environment *ev,
	  gpointer user_data)
{
	GtkIPEntry *ipentry;
	guint8 octets[4];
	
	g_return_if_fail (IS_GTK_IP_ENTRY(user_data));
		
	/*
	 * get all the octets from our GtkIPEntry widget
	 */
	ipentry = GTK_IP_ENTRY (user_data); 
	gtk_ip_entry_get_octets (ipentry, octets);

	/*
	 * assign the desired octet to the arg out-parameter
	 */
	BONOBO_ARG_SET_INT (arg, octets[arg_id]);	
}


/*
 * put a value in one of the gtkipentry octets
 */
static void
set_prop (BonoboPropertyBag *bag,
	  const BonoboArg *arg,
	  guint arg_id,
	  CORBA_Environment *ev,
	  gpointer user_data)
{
	guint8 octets[4];
	GtkIPEntry *ipentry = GTK_IP_ENTRY (user_data);

	/*
	 * retrieve the old values first,
	 * as we cannot set octets separately,
	 * then change one of them, an store them
	 */
	gtk_ip_entry_get_octets (ipentry, octets);
	octets[arg_id] = BONOBO_ARG_GET_INT (arg);
	gtk_ip_entry_set_octets (ipentry, octets); 
}
	
We can then register these two accessor functions. We can do this by extending the bonobo_ipctrl_factory we previously discussed with the following:
/* 
 * create a property bag:
 * we provide our accessor functions for properties, and 
 * the gtk widget
 * */
prop_bag = bonobo_property_bag_new (get_prop, set_prop, ipentry);
bonobo_control_set_properties (control, prop_bag);


/* put some properties in the property bag */
bonobo_property_bag_add (prop_bag, "octet1", 0, BONOBO_ARG_INT, 
			 NULL, "octet1", 0);
bonobo_property_bag_add (prop_bag, "octet2", 1, BONOBO_ARG_INT, 
		         NULL, "octet2", 0);
bonobo_property_bag_add (prop_bag, "octet3", 2, BONOBO_ARG_INT, 
			 NULL, "octet3", 0);
bonobo_property_bag_add (prop_bag, "octet4", 3, BONOBO_ARG_INT, 
		         NULL, "octet4", 0);

	
As you can see, there are a couple of things you need to do. First, you need to create a BonoboPropertyBag-object. Second, you need to connect this object to the control using the bonono_control_set_property_bag. Now, you can an arbitrary number of properties to this property bag. Rather boring, we only add the four octets we need for our control. I trust you can figure out all kinds of interesting variants.

Notifying listeners

It is often the case that the container application wants to do something when one or more of the values in the property bag change. The container could of course continuously poll the control's property bag for changes, but that's not a very nice solution. What's better is to have the control notify the container when something has changed.

The control's side of the story is very straightforward. We add a function notify_property_bag:
/*
 *  notify listeners to this property bag that something has
 *  changed
 */
static void
notify_property_bag (GtkIPEntry *ipentry, gpointer data)
{
	guint8 octets[4];
	BonoboPropertyBag *prop_bag;
	BonoboArg *arg;
	
	g_return_if_fail (BONOBO_IS_PROPERTY_BAG(data));
	prop_bag = BONOBO_PROPERTY_BAG (data);

	/*
	 * arg is only a dummy argument, as we only want to
	 * notify the container *something* has changed
	 */  
	arg = bonobo_arg_new (BONOBO_ARG_INT);
	BONOBO_ARG_SET_INT (arg,0);


	/*
	 * notify our listeners that something has changed
	 */
	bonobo_property_bag_notify_listeners (prop_bag, "octet1", arg, NULL);
}
	
Note the final line of the function. We only notify the function that "octet1" of the control has changed - this is bit hackish. What we'd really like to express is that something changed. There's no way to do that, so we simple notify all listeners that "octet1" has changed. The listener can then simply update all. Note that bonobo_property_bag_notify_listeners enables nice model-view patterns: you can have one property bag, and many listeners (even on different machines), and when anything changes, all listeners are notified.

Now, the final control-side step is to connect the "change" signal from the GTKIPEntry to this function. So, in bonobo_ipctrl_factory, we add:
/* connect the GtkIPEntry's signal */
gtk_signal_connect (GTK_OBJECT(ipentry), 
		    "changed", 
		    notify_property_bag, 
		    prop_bag);
	
And that's all we need to do for the server-side (ipctrl-side) of the propertybag listener.

Registration using the Object Activation Framework

After creating our control, we need to tell the world we did. For applications to be able to find the control and activate it, we can use the Object Activation Framework (OAF).[2] [2]

The Object Activation Framework (OAF) is a library that takes care of activating CORBA servers on request. Based on your settings, it will locate the server, check whether the server is already running, start it if necessary etc. Note that 'CORBA server' means some 'thing' that services CORBA requests. For example, our beloved IPCtrl is a CORBA server (when running). So how does OAF do all the magic? Well, we need to register our control. This is accomplished by making a .oaf file: OAF uses little XML-like files (with '.oaf'-extension) that specify things about CORBA servers (not just Bonobo CORBA servers). Although .oaf-files look like XML, OAF is not too happy with XML-style comments. In fact, when the .oaf-files are commented, they won't work.However, for the sake of learning, I'll show the heavy commented version; the OAF-file we will actually be using will be same, but without the comments.

To ensure there won't be multiple Bonobo-controls or OAF-files with the same name, it's important to honor the Bonobo namespacing rules. The rules are described in the Bonobo FAQ and NAMESPACING in the the doc/ subdirectory of the Bonbobo source distribution. Please read them before releasing any of your Bonobo controls to the world! In this case, we choose 'GNOME_'-appname-controlname, ie. GNOME_BonoboTutorial_IPCtrl. We choose this prefix for both the OAFIID's and the .oaf file names.

  
<!--
*** WARNING ***: this file (GNOME_BonoboTutorial_IPCtrl.oaf.commented)
is only for educational purposes; OAF doesn't like comments, so this
won't work if you try to install it.  install ipctr.oaf instead.
-->


<!-- 
OAF maps a component's symbolic name to an 'activated instance' of a
CORBA server, is this case a Bonobo component.

An 'activated instance' can be some already-running instance, but can
also involve activating a new instance from some executable file or
shared library.

An .oaf file (such as this one) is in an XML-based format, and
specifies how to activate this component.

Two oaf_server's are specified: one for the component itself, and one
for its factory.

-->


<oaf_info>		

  <!-- this oaf_server describes a *factory* for producing ipctrl's;
  the factory is of type 'exe', which means that we can start it by
  executing the executable file specified in the 'location' attribute
  -->
 
  <oaf_server
	      iid="OAFIID:GNOME_BonoboTutorial_IPCtrl_ControlFactory"
	      type="exe" location="/home/binnema/bin/bonobo-ipctrl">
    
    <!-- you can add arbitrary 'oaf_attributes' to component; OAF
    allows you to query for such attributes. This is very powerful;
    for example, you could a number of components with .oaf-files that
    specify the 'foo' attribute, and then ask OAF to activated the
    component with the highest 'foo-value'.

    in this case, we only specify the de-facto-standard 'name' and
    'description' attributes.
    -->

    <oaf_attribute name="repo_ids" type="stringv">
      <item value="IDL:Bonobo/GenericFactory:1.0" />
    </oaf_attribute>

    <oaf_attribute name="name" type="string" value="IPEntry Control Factory" />
    <oaf_attribute name="description" type="string" value="Factory for an IPEntry Control" />

  </oaf_server>

  
  
  <!--   this oaf_server describes the ipctrl itself; it's of the type
  'factory', which meanse that ipctrls are produced by some
  factory. This factory is fact the factory object we specified
  above. So here we tell OAF that ipctrls can be instantiated by
  invoking the bonobo_ipctrl_factory component.
  -->
  <oaf_server
	      iid="OAFIID:GNOME_BonoboTutorial_IPCtrl_Control"
	      type="factory"
	      location="OAFIID:GNOME_BonoboTutorial_IPCtrl_ControlFactory">
    
    <oaf_attribute name="repo_ids" type="stringv">
      <item value="IDL:Bonobo/Unknown:1.0" /> 
	<item value="IDL:Bonobo/Control:1.0" />
    </oaf_attribute>
    
    <oaf_attribute name="name" type="string"
		   value="Bonobo control" />
    
    <oaf_attribute name="description" type="string"
		   value="This is a IPEntry Control." />
  </oaf_server>
</oaf_info>
	

Without focusing on the details too much (you can take a look at the ipctrl.oaf file), there are a couple of important things here. Remember that our ipctrl.c defines a control factory? Well, it contains the line:
	/* register the factory (using OAF) */
	factory = bonobo_generic_factory_new
		("OAFIID:GNOME_BonoboTutorial_IPCtrl_ControlFactory",
		 bonobo_ipctrl_factory, NULL);
	
This registers the control factory with OAF. The container application (which will be discussed shortly) has a corresponding line:
	/* get a widget, containing the control */
	control = bonobo_widget_new_control
		("OAFIID:GNOME_BonoboTutorial_IPCtrl_Control",
		 BONOBO_OBJREF (uic));
	

The GNOME_BonoboTutorial_IPCtrl.oaf is an XML-file that you should put in some directory where OAF can find it; depending on your OAF installation settings this could be /opt/share/oaf/GNOME_BonoboTutorial_ipctrl.oaf or /usr/share/oaf/GNOME_BonoboTutorial_ipctrl.oaf or /usr/local/share/oaf/GNOME_BonoboTutorial_ipctrl.oaf, or any other place. Check your OAF installation. Another way is to use the OAF_INFO_PATH-environment variable to point to the directories OAF should search. Thus, you don't need to be root. Note that putting anything but absolute paths in OAF_INFO_PATH is usually a bad idea, as the oafd daemon that carries out your request, is running as a separate process, and has its own current working directory etc. Thus, using relative paths or other smartness wont work most of the time. Management summary: always use absolute, full paths. Finally, to use the ipctrl.oaf in your local situation, change the 'location=' attribute to point to the place you keep the ipctrl.

There are many more nice things you can do with OAF, please refer to the fine OAF documentation written by Mathieu Lacage (comes with the OAF source distribution (the documentation, that is ;-))

Notes

[1]

Why do we need a factory? Can't we just create the object? Well yes, we could, but by separating the creation issue from the control's application logic, we gain a lot of flexibility.

[2]

Currently, GNOME uses the gnorba-library to find and activate CORBA objects; however, gnorba will be replaced by OAF.