]> The GNOME Panel Libraries Jacob Berkman
jacob@helixcode.com
George Lebl
jirka@5z.com
2000 The Free Software Foundation 2000 Helix Code, Inc. 2000 Eazel, Inc.
The Art of Applet Writing Introduction Applets are basically GNOME applications whose window sits inside the panel. Also the panel "takes care" of the applets by providing them with session saving and restarting, window management (inside of the panel), and a context menu. The Hello World of Applets The simplest applet one can write would be along the lines of: hello-world_applet #include <config.h> #include <applet-widget.h> int main(int argc, char **argv) { GtkWidget *applet; GtkWidget *label; /* Initialize the i18n stuff */ bindtextdomain (PACKAGE, GNOMELOCALEDIR); textdomain (PACKAGE); /* intialize, this will basically set up the applet, corba and call gnome_init */ applet_widget_init("hello-applet", NULL, argc, argv, NULL,0,NULL); /* create a new applet_widget */ applet = applet_widget_new("hello-applet"); /* in the rare case that the communication with the panel failed, error out */ if (!applet) g_error("Can't create applet!\n"); /* create the widget we are going to put on the applet */ label = gtk_label_new(_("Hello There!")); gtk_widget_show(label); /* add the widget to the applet-widget, and thereby actually putting it "onto" the panel */ applet_widget_add (APPLET_WIDGET (applet), label); gtk_widget_show (applet); /* special corba main loop */ applet_widget_gtk_main (); return 0; } This creates an applet which just sits on the panel, not really doing anything, in real life the label would be substituted by something which actually does something useful. As you can see the applet doesn't really take care of restarting itself. The .gnorba and .desktop Files For the applet to be added to the menus, you need to install two files. Your x.gnorba file goes into $sysconfdir/CORBA/servers/ and the x.desktop file goes into $prefix/share/applets/<category>/. Example files are: hello.desktop [Desktop Entry] Name=Hello Applet Comment=An example Hello World type Applet Type=PanelApplet Exec=hello_applet Icon=gnome-hello.png Terminal=0 hello.gnorba [hello_applet] type=exe repo_id=IDL:GNOME/Applet:1.0 description=Hello Applet location_info=hello_applet One thing to keep in mind is that the Exec line for the .desktop doesn't actually get executed when the Type is PanelApplet. The Exec line should be the GOAD ID specified in the .gnorba file (the "hello_applet" enclosed by brackets). For a simple applet all you need to do is replace the hello_applet with the name of your applet executable. The Applet's Context Menu When the user right clicks on the applet, a menu appears, this is all handeled by the panel, so in order to add items to it you use a special interface to "add callbacks" to the menu. A very simple example would be (making our hello applet even more feature full): hello_applet with menu items #include <config.h> #include <applet-widget.h> static void hello_there (AppletWidget *applet, gpointer data) { g_print(_("Hello There")); } int main (int argc, char **argv) { GtkWidget *applet; GtkWidget *label; /* Initialize the i18n stuff */ bindtextdomain (PACKAGE, GNOMELOCALEDIR); textdomain (PACKAGE); /* intialize, this will basically set up the applet, corba and call gnome_init */ applet_widget_init("hello_applet", NULL, argc, argv, NULL, 0, NULL); /* create a new applet_widget */ applet = applet_widget_new("hello_applet"); /* in the rare case that the communication with the panel failed, error out */ if (!applet) g_error("Can't create applet!\n"); /* create the widget we are going to put on the applet */ label = gtk_label_new(_("Hello There!")); gtk_widget_show(label); /* add the widget to the applet-widget, and thereby actually putting it "onto" the panel */ applet_widget_add (APPLET_WIDGET (applet), label); gtk_widget_show (applet); /* add an item to the applet menu */ applet_widget_register_callback(APPLET_WIDGET(applet), "hello", _("Hello There"), hello_there, NULL); /* special corba main loop */ applet_widget_gtk_main (); return 0; } Now the user will see a "Hello There" menu item on the applet menu, and when selected, the applet will print "Hello There". Useful huh? Note that the second argument to the register_callback is just a string identifier of this callback, and can really be whatever you want. But it should NOT be translated as the label (the 3rd argument) should be. Advanced Menu Stuff It is also possible to have submenus, remove menus and use gnome-stock icons on the menus. Submenus To do submenus, you have to first call applet_widget_register_callback_dir, which only takes the callback name and the menu text. The callback name should end with '/'. The callback name works as a "path" for the submenus. So to add a submenu "Foo" and in item "Bar" (into the submenu "Foo") you would do: Adding a Submenu applet_widget_register_callback_dir (APPLET_WIDGET (applet), "foo", _("Foo")); applet_widget_register_callback (APPLET_WIDGET (applet), "foo/bar", _("Bar"), bar_callback, NULL); Deleting To delete a menu item, just call applet_widget_unregister_callback or applet_widget_unregister_callback_dir, with the proper callback name. Stock Icons You use the _stock derivatives of the callback functions and pass an extra argument with the GNOME_STOCK_MENU_* type. For example to add an about menu item: Adding Items with Stock Pixmaps applet_widget_register_stock_callback(APPLET_WIDGET(applet), "about", GNOME_STOCK_MENU_ABOUT, _("About..."), about_cb, NULL); Session Saving The panel is session manager aware but the applets don't have to be, they can depend on the panel to save their information in a proper place. Basically session saving has two parts, loading the info, and saving the info. Loading is pretty simple, after you do applet_widget_new, you can get the correct paths to load your properties from the widget's structure. For example: Getting Private Data gnome_config_push_prefix(APPLET_WIDGET(applet)->privcfgpath); hello = gnome_config_get_bool("section/hello=true"); gnome_config_pop_prefix(); will do the trick. For saving it's a little bit more complicated but not by much, let's make our original example save a global variable hello. Saving Private HelloWorld #include <config.h> #include <applet-widget.h> /* useless variable that we want to save the state of*/ gint hello = TRUE; /* sesion save signal handler*/ static gint applet_save_session(GtkWidget *w, const char *privcfgpath, const char *globcfgpath) { gnome_config_push_prefix(privcfgpath); gnome_config_set_string("section/hello",hello); gnome_config_pop_prefix(); gnome_config_sync(); /* you need to use the drop_all here since we're all writing to one file, without it, things might not work too well */ gnome_config_drop_all(); /* make sure you return FALSE, otherwise your applet might not work compeltely, there are very few circumstances where you want to return TRUE. This behaves similiar to GTK events, in that if you return FALSE it means that you haven't done everything yourself, meaning you want the panel to save your other state such as the panel you are on, position, parameter, etc ... */ return FALSE; } int main(int argc, char **argv) { GtkWidget *applet; GtkWidget *label; /* Initialize the i18n stuff */ bindtextdomain (PACKAGE, GNOMELOCALEDIR); textdomain (PACKAGE); /* intialize, this will basically set up the applet, corba and call gnome_init */ applet_widget_init("hello_applet", NULL, argc, argv, NULL, 0, NULL); /* create a new applet_widget */ applet = applet_widget_new("hello_applet"); /* in the rare case that the communication with the panel failed, error out */ if (!applet) g_error("Can't create applet!\n"); /* read the contents of the stored value of hello from the config file */ gnome_config_push_prefix(APPLET_WIDGET(applet)->privcfgpath); hello = gnome_config_get_bool("section/hello=true"); gnome_config_pop_prefix(); /* create the widget we are going to put on the applet */ label = gtk_label_new(_("Hello There!")); gtk_widget_show(label); /* bind the session save signal */ gtk_signal_connect(GTK_OBJECT(applet),"save_session", GTK_SIGNAL_FUNC(applet_save_session), NULL); /* add the widget to the applet-widget, and thereby actually putting it "onto" the panel */ applet_widget_add (APPLET_WIDGET (applet), label); gtk_widget_show (applet); /* special corba main loop */ applet_widget_gtk_main (); return 0; } That's basically it. Make sure you return FALSE from the save_session handler, else the panel will not remember your applet next time. Also note the presence of gnome_config_drop_all, that needs to be done, especially for multi applets (discussed below), or your info might get lost. If you need to store information global to all applets you can use the globcfgpath counterpart of privcfgpath, which gives you a path to a file which is the same for all applets. IMPORTANT! Make sure you only use two levels of config path below privcfgpath/globcfgpath. Which means you only tack on "section/key". Also don't just use "key". You need to tack on both the section and the key, no more, no less. Retrieving Global Data gnome_config_push_prefix(APPLET_WIDGET(applet)->globcfgpath); hello = gnome_config_get_bool("all_hello_applets/hello=true"); gnome_config_pop_prefix(); Similarly for the save_session. Note: When you update your configuration in some properties dialog, or however lse, you should call applet_widget_sync_config (AppletWidget *applet), it will tell the panel to send a session save signal to the applet with the correct paths etc. This is not 100% neccessary, but makes it nice so that configuration is not lost during crashes (when the panel couldn't do it's complete save during shutdown) [ed: you should still do this, even though the panel doesn't crash anymore] Panel Orientation How to tell which way the panel on which your applet sits is fairly simply. You bind the "change_orient" signal of the applet. To modify our original hello applet, we'd do: Freshman Orientation #include <config.h> #include <applet-widget.h> /*this is when the panel orientation changes*/ static void applet_change_orient(GtkWidget *w, PanelOrientType o, gpointer data) { switch(o) { case ORIENT_UP: puts("ORIENT UP"); break; case ORIENT_DOWN: puts("ORIENT DOWN"); break; case ORIENT_LEFT: puts("ORIENT LEFT"); break; case ORIENT_RIGHT: puts("ORIENT RIGHT"); break; } } int main(int argc, char **argv) { GtkWidget *applet; GtkWidget *label; /* Initialize the i18n stuff */ bindtextdomain (PACKAGE, GNOMELOCALEDIR); textdomain (PACKAGE); /* intialize, this will basically set up the applet, corba and call gnome_init */ applet_widget_init("hello_applet", NULL, argc, argv, NULL, 0, NULL); /* create a new applet_widget */ applet = applet_widget_new("hello_applet"); /* in the rare case that the communication with the panel failed, error out */ if (!applet) g_error("Can't create applet!\n"); /* create the widget we are going to put on the applet */ label = gtk_label_new(_("Hello There!")); gtk_widget_show(label); /*we have to bind change_orient before we do applet_widget_add since we need to get an initial change_orient signal to set our initial oriantation, and we get that during the _add call*/ gtk_signal_connect(GTK_OBJECT(applet),"change_orient", GTK_SIGNAL_FUNC(applet_change_orient), NULL); /* add the widget to the applet-widget, and thereby actually putting it "onto" the panel */ applet_widget_add (APPLET_WIDGET (applet), label); gtk_widget_show (applet); /* special corba main loop */ applet_widget_gtk_main (); return 0; } Panel Size (Note: this is not included in versions of the panel prior to 1.2) One new feature of the panel is the size support. The panel supports the following sizes: Tiny: 24 pixels Small: 36 pixels Standard: 48 pixels Large: 64 pixels Huge: 80 pixls It would be nice to let your applet pick it's layout so that it doesn't stretch the panel out of it's preffered size (the panel is always as thick as the thickest applet) The way this works is very similiar to the way orientation works. You bind the "change_pixel_size" signal to the applet, so to modify our original hello applet, we'd do: The Sizes, The Keep A-Changin' #include <config.h> #include <applet-widget.h> #ifdef HAVE_PANEL_PIXEL_SIZE /*this is when the panel size changes*/ static void applet_change_pixel_size(GtkWidget *w, int size, gpointer data) { printf("Got size of %d pixels\n",size); } #endif int main(int argc, char **argv) { GtkWidget *applet; GtkWidget *label; /* Initialize the i18n stuff */ bindtextdomain (PACKAGE, GNOMELOCALEDIR); textdomain (PACKAGE); /* intialize, this will basically set up the applet, corba and call gnome_init */ applet_widget_init("hello_applet", NULL, argc, argv, NULL, 0, NULL); /* create a new applet_widget */ applet = applet_widget_new("hello_applet"); /* in the rare case that the communication with the panel failed, error out */ if (!applet) g_error("Can't create applet!\n"); /* create the widget we are going to put on the applet */ label = gtk_label_new(_("Hello There!")); gtk_widget_show(label); #ifdef HAVE_PANEL_PIXEL_SIZE /*we have to bind change_pixel_size before we do applet_widget_add since we need to get an initial change_pixel_size signal to set our initial size, and we get that during the _add call*/ gtk_signal_connect(GTK_OBJECT(applet),"change_pixel_size", GTK_SIGNAL_FUNC(applet_change_pixel_size), NULL); #endif /* add the widget to the applet-widget, and thereby actually putting it "onto" the panel */ applet_widget_add (APPLET_WIDGET (applet), label); gtk_widget_show (applet); /* special corba main loop */ applet_widget_gtk_main (); return 0; } Notice the "#ifdef HAVE_PANEL_PIXEL_SIZE" line, this will make sure your applet compiles correctly even on a panel from gnome-core 1.0 which doesn't have support for multiple sizes. Note that in gnome-core 1.1.0 release there was another implementation of panel sizes which is now deprecated, so you should use the method above. If you want to say compare to the standard sizes (You shouldn't assume that they are the only ones that exist!), you can use the PIXEL_SIZE_TINY, PIXEL_SIZE_STANDARD, PIXEL_SIZE_LARGE and PIXEL_SIZE_HUGE constants as in: Using Standard Pixel Sizes if(size < PIXEL_SIZE_STANDARD) { ... do something for very small applet ... } else { ... do something else for standard size applet ... } Rebinding Events (Note: this is not included in versions of the panel prior to 1.2) Sometimes you want to change the way the applet looks after it has already been added to the panel, and you want the right and middle mouse button clicks to still work. In this case you need to notify the panel that it should try to rebind the events. You do this with: Rebinding #ifdef HAVE_APPLET_BIND_EVENTS applet_widget_bind_events(APPLET_WIDGET(applet),GTK_WIDGET(widget)); #endif which will bind mouseclicks (2nd and 3rd button) on the "widget". Note that this is NOT in the panel in gnome-core 1.0, so if you use this feature, make sure to put the "#ifdef HAVE_APPLET_BIND_EVENTS" around the code. Multiple Applet Support Having one process per applet might be ok, but when you have many applets it can be quite a hit on the memory. There is an easy way to multiple applets from one process, even different types of applets. For a simple example let's modify our original hello applet to make it possible to have multiple instances of it from just one executable. We will create a factory corba service that can create new instances of the applet. Industrialization of hello_applet #include <config.h> #include <applet-widget.h> /*when we get a command to start a new widget*/ static GtkWidget * applet_start_new_applet(const gchar *goad_id, const gchar **params, gint nparams) { GtkWidget *applet; GtkWidget *label; /*if we weren't asked to start hello_applet, just return*/ if(strcmp(goad_id, quot;hello_appletquot;)!=0) return NULL; /*now we do the same exact thing as we do in the main function for creating the applet*/ /* create a new applet_widget */ applet = applet_widget_new(quot;hello_appletquot;); if (!applet) g_error(quot;Can't create applet!\nquot;); /* create the widget we are going to put on the applet */ label = gtk_label_new(_(quot;Hello There!quot;)); gtk_widget_show(label); /* add the widget to the applet-widget, and thereby actually putting it quot;ontoquot; the panel */ applet_widget_add (APPLET_WIDGET (applet), label); gtk_widget_show (applet); /* return the applet widget from this function */ return applet; } int main(int argc, char **argv) { GtkWidget *applet; GtkWidget *label; gchar *goad_id; /* Initialize the i18n stuff */ bindtextdomain (PACKAGE, GNOMELOCALEDIR); textdomain (PACKAGE); /* intialize, this will basically set up the applet, corba and call gnome_init */ applet_widget_init(quot;hello_appletquot;, NULL, argc, argv, NULL, 0, NULL); /*make new factory and get us the goad_id that was used to start us*/ applet_factory_new(quot;hello_applet_factoryquot;, NULL, applet_start_new_applet); goad_id = (gchar *)goad_server_activation_id(); /*if the goad_id was hello_applet, we create a new instance of our applet, otherwise don't do anything*/ if(goad_id && strcmp(goad_id, quot;hello_appletquot;)==0) { /* create a new applet_widget */ applet = applet_widget_new(quot;hello_appletquot;); if (!applet) g_error(quot;Can't create applet!\nquot;); /* create the widget we are going to put on the applet */ label = gtk_label_new(_(quot;Hello There!quot;)); gtk_widget_show(label); /* add the widget to the applet-widget, and thereby actually putting it quot;ontoquot; the panel */ applet_widget_add (APPLET_WIDGET (applet), label); gtk_widget_show (applet); } /* special corba main loop */ applet_widget_gtk_main (); return 0; } What you will notice is that what we do is just make a factory service with applet_factory_new, to which we pass a function pointer to a function that just creates new applets for us. Now we need to create a .gnorba and .desktop files for an applet of this type. The .desktop file is the exact same as for normal applets. The .gnorba file however must now describe the factory as well. Industrialization of hello.gnorba [hello_applet_factory] type=exe repo_id=IDL:GNOME/GenericFactory:1.0 description=Hello Applet location_info=hello_applet [hello_applet] type=factory repo_id=IDL:GNOME/Applet:1.0 description=Hello Applet location_info=hello_applet_factory Sometimes you may want to have two applets that have very similiar functionality, but that appear to the user as two different applets, and you want to manage them from the same process. This is extremely simple. Just take the above example and add more types into the .gnorba file, then wherever we check the goad_id, just add another "else if" to check for another goad_id. Then in your desktops on the Exec line, you would have in one .desktop: Exec=hello_version_1_applet and in a second: Exec=hello_version_2_applet Shared Library Applets It is possible to make applets which will not be separate processes, but will be loaded directly into the panel. This saves on memory, but can make the panel less stable if the applet is less stable. Here is an example, George will comment on stuff unless I sleep: piCommander #include <config.> #include <applet-widget.h> typedef struct { GtkWidget *applet; GtkWidget *fentry; GtkWidget *gentry; GtkWidget *entry; } PicoData; static void entry_activate (GtkWidget *w, PicoData *pico) { char *s; s = gtk_editable_get_chars (GTK_EDITABLE (pico->entry), 0, -1); gtk_editable_delete_text (GTK_EDITABLE (pico->entry), 0, -1); if (strlen (s) == 0) { g_free (s); return; } gnome_execute_shell (NULL, s); gnome_entry_save_history (GNOME_ENTRY (pico->gentry)); gnome_config_sync (); g_free (s); } static int applet_save_session (GtkWidget *applet, const char *privcfgpath, const char *globcfgpath, PicoData *pico) { gnome_entry_save_history (GNOME_ENTRY (pico->gentry)); gnome_config_sync (); return FALSE; } static void applet_destroy (GtkWidget *applet, PicoData *pico) { g_free (pico); } static CORBA_Object pico_begin (PortableServer_POA poa, const char *goad_id, const char **params, gpointer *impl_ptr, CORBA_Environment *ev) { PicoData *pico = g_new0 (PicoData, 1); pico->applet = applet_widget_new (goad_id); pico->fentry = gnome_file_entry_new ("pico-commander", _("Run...")); pico->gentry = gnome_file_entry_gnome_entry (GNOME_FILE_ENTRY (pico->fentry)); pico->entry = gnome_file_entry_gtk_entry (GNOME_FILE_ENTRY (pico->fentry)); gnome_entry_set_max_saved (GNOME_ENTRY (pico->gentry), 50); gnome_entry_prepend_history (GNOME_ENTRY (pico->gentry), FALSE, ""); gtk_combo_set_use_arrows_always (GTK_COMBO (pico->gentry), TRUE); applet_widget_add (APPLET_WIDGET (pico->applet), pico->fentry); gtk_signal_connect (GTK_OBJECT (pico->entry), "activate", GTK_SIGNAL_FUNC (entry_activate), pico); gtk_signal_connect (GTK_OBJECT (pico->applet), "save_session", GTK_SIGNAL_FUNC (applet_save_session), pico); gtk_signal_connect (GTK_OBJECT (pico->applet), "destroy", GTK_SIGNAL_FUNC (applet_destroy), pico); gtk_widget_show_all (pico->applet); return applet_widget_corba_activate (APPLET_WIDGET (pico->applet), poa, goad_id, params, impl_ptr, ev); } static void pico_end (PortableServer_POA poa, const char *goad_id, gpointer impl_ptr, CORBA_Environment *ev) { applet_widget_corba_deactivate (poa, goad_id, impl_ptr, ev); } static const char *repo_id[] = { "IDL:GNOME/Applet:1.0", NULL }; static GnomePluginObject applets_list[] = { { repo_id, "pico-commander_applet", NULL, "50 line launcher", &pico_begin, &pico_end }, { NULL } }; GnomePlugin GNOME_Plugin_info = { applets_list, NULL }; Building the Applets Here's a simple makefile you can use (this one is for the fish applet) if you want to compile applets outside of the gnome-core source tree. It was sent to me by John Ellis <johne@bellatlantic.net> FIXME! More up-to-date example. GNOME Panel Applet Library This describes the API for the GNOME panel applet library, as of the 1.2.0 release. &panel-init; &panel-interact; &panel-menu; &panel-misc; GNOME Status Docklet Library This describes the API for the GNOME status docklet library, as of the 1.2.0 release of gnome-core. &panel-status-docklet;