User Communication: Dialogs

Table of Contents
The GnomeDialog Widget
Modal Dialogs
A Dialog Example
Special Dialog Types
Convenience Routines

Dialogs are a continuous annoyance in plain GTK+; every time you want to tell the user anything, you have to create a window, create some buttons, create a label, pack the buttons and label into the window, set up callbacks, remember to capture "delete_event", and so on. It's a pain. Gnome saves you from this pain, with an easy-to-use general-purpose dialog widget, and several subclasses of it that implement common dialog types. Gnome also has easy functions for using modal dialogs.

The GnomeDialog Widget

Since dialogs in plain GTK+ are painstakingly constructed from scratch, there are at least as many ways to write a dialog as there are programmers. The programmer must decide where to place the dialog on the screen, how much padding to have, whether to put a separator above the buttons, what container to put the buttons in, what the keyboard shortcuts are, and so on. The premise of GnomeDialog is that the programmer should not have to care about these things; if they're variable at all, the user should configure them the way they want. From the programmer's perspective, dialogs "just work."

Creating a Dialog

A GnomeDialog is easy to create. Here's a summary of the basic steps, more detail follows:

  1. Read the section called Special Dialog Types and decide whether one of the special dialog subclasses is appropriate. If so, skip the below steps and create that subclass instead.

  2. Create the widget with gnome_dialog_new(). Pass this function the title of the dialog (displayed by the window manager) and the name of each button you'd like to have.

  3. Populate GNOME_DIALOG(dialog)->vbox with the contents of your dialog.

  4. Plan how your dialog will work. You can connect to the "close" or "clicked" signals, as appropriate. You can have the dialog hide or destroy itself when closed. You can also have the dialog automatically close when clicked, or handle this yourself. There are a number of ways the user can interact with a dialog, so it's important to be sure the combination of settings you choose will work no matter what the user does.

To create a dialog, use gnome_dialog_new(), shown in Figure 1. The argument list is a NULL-terminated list of buttons to insert in the dialog. For example, you might say:


GtkWidget* dialog;
dialog = gnome_dialog_new(_("My Dialog Title"),
                          _("OK"),
                          _("Cancel"),
                          NULL);

      

This creates a dialog titled "My Dialog Title" with an OK and a Cancel button; the strings are marked for translation with the _() macro. The OK button will be the leftmost button in the dialog.

       #include <libgnomeui/gnome-dialog.h>
      

GtkWidget* gnome_dialog_new(const gchar* title, ...);

Figure 1. GnomeDialog Constructor

The GnomeDialog API numbers the buttons you add starting with 0; you use these numbers to refer to the buttons later, since you don't have a pointer to the automatically-created button widgets. In this case, the OK button is button 0, and the Cancel button is button 1. (Note that this is standard Gnome practice---OK or Yes goes first, then Cancel or No. In fact libgnomeui/gnome-uidefs.h contains the macros GNOME_YES, GNOME_OK, GNOME_NO, and GNOME_CANCEL which represent the dialog button numbers for these items in a two-button dialog.)

The above example, which specifies buttons called "OK" and "Cancel," is not quite correct for production code. Gnome provides a set of "stock buttons" for common button names. These ensure everyone uses "OK" instead of "Ok" or "OK!"; they allow translators to translate common strings only once; and they often insert icons in the buttons, making them more attractive and recognizable to users. You should always use stock buttons if possible.

You can use stock buttons in gnome_dialog_new(). Simply substitute the stock button macros for the button names:


dialog = gnome_dialog_new(_("My Dialog Title"),
                          GNOME_STOCK_BUTTON_OK,
                          GNOME_STOCK_BUTTON_CANCEL,
                          NULL);

      

Gnome includes many stock buttons, stock menu items, and stock pixmaps---it's a good idea to check these out so you don't reinvent the wheel. There's a complete list in libgnomeui/gnome-stock.h.

Filling in the Dialog

After creating a dialog, you'll want to put something inside. If you just want a label inside, probably you should use GnomeMessageBox or one of the convenience routines (such as gnome_ok_dialog()) instead of constructing the dialog manually. Otherwise, filling a dialog is very simple:


GtkWidget* button;
/* ... create dialog as shown earlier ... */
button = gtk_button_new_with_label(_("Push Me"));
gtk_box_pack_start(GTK_BOX(GNOME_DIALOG(dialog)->vbox)),
                   button, 
                   TRUE, 
                   TRUE,
                   0);
      

Of course you can pack the contents of dialog->vbox using the packing options of your choice. The above code is just an example.

Figure 2 shows a dialog from the Gnumeric spreadsheet, with its components labelled.

Figure 2. A GnomeDialog from the Gnumeric spreadsheet

Handling GnomeDialog Signals

Now for the tricky part. You have to prepare yourself to handle anything the user might do to your dialog. Here's a brief list of possibilities; it's worth going over the list whenever you create a dialog:

  • Closing the dialog by pressing the Escape key

  • Closing the dialog by clicking the window manager's close decoration

  • Clicking one of the dialog's buttons

  • Interacting with the contents of the dialog

  • If the dialog is not modal, interacting with other parts of the application

GnomeDialog emits two signals in addition to those it inherits from parent classes. If the user clicks one of the dialog's buttons, a "clicked" signal is emitted. (This is not the "clicked" signal from GtkButton; it's a different signal, emitted by GnomeDialog.) A GnomeDialog"clicked" handler should have three arguments: the dialog emitting the signal, the number of the button clicked, and your callback data.

GnomeDialog also has a "close" signal. It is emitted when gnome_dialog_close() is called; all the built-in event handlers (e.g. for the Escape shortcut) call this function to close the dialog. GnomeDialog's default handler for "close" has two possible behaviors: it can call either gtk_widget_hide() or gtk_widget_destroy() on the dialog. The behavior is configurable by calling gnome_dialog_close_hides(), shown in Figure 3.

       #include <libgnomeui/gnome-dialog.h>
      

void gnome_dialog_close_hides(GnomeDialog* dialog, gboolean setting);

void gnome_dialog_set_close(GnomeDialog* dialog, gboolean setting);

Figure 3. Closing GnomeDialog

By default, "close" destroys the dialog. This is what you usually want; however, if a dialog is noticeably time-consuming to create, you might want to merely hide and re-show it between uses, without ever destroying it. You might also want to hide the dialog from the user, extract the state of any widgets inside it, and then destroy it with gtk_widget_destroy(). The decision depends on the structure of your code. However, in general it is simpler and less error-prone to let the dialog be destroyed when clicked. You can connect to the "clicked" signal if you need to query the state of widgets in the dialog.

If you connect a handler to "close", that handler should return a boolean value. If it returns TRUE, the hide or destroy will not take place. You can use this to keep the user from closing the dialog, for example if they have not filled in all the fields of a form.

The "close" signal is designed to collect several possible user actions into a single handler: it should be emitted when the user presses Escape or the window manager's window close button is clicked. It's often convenient to emit close when the dialog's buttons are clicked as well. You can ask GnomeDialog to emit close whenever a button is clicked with gnome_dialog_set_close() (Figure 3); if its setting argument is TRUE, the dialog will emit "close" in addition to "clicked" if any of its buttons are clicked. By default, this setting is FALSE for GnomeDialog, but for many of the special dialog types the default is TRUE (the inconsistency is an unfortunate misfeature).

Note that the "close" signal is emitted when the dialog receives "delete_event"; this means you only have to write one signal handler to deal with all dialog closings. There is no need to handle "delete_event" as a separate case.

Finishing Touches

The difference between a good dialog and a great dialog is in the details. GnomeDialog comes with a number of features to make that final polish easy. Figure 4 sums them up.

       #include <libgnomeui/gnome-dialog.h>
      

void gnome_dialog_set_parent(GnomeDialog* dialog, GtkWindow* parent);

void gnome_dialog_set_default(GnomeDialog* dialog, gint button);

void gnome_dialog_editable_enters(GnomeDialog* dialog, GtkEditable* editable);

void gnome_dialog_set_sensitive(GnomeDialog* dialog, gint button, gboolean setting);

Figure 4. GnomeDialog Polish

Dialogs have a logical parent, usually the main application window. You can tell the library about this parent-child relationship; this lets Gnome honor certain user preferences, and in turn indicates the relationship to the window manager. Most window managers will minimize child dialogs when the parent window is minimized, and keep child dialogs on top of their parent.

It's important to use gnome_dialog_set_parent() with transient dialogs only. A transient dialog is one that appears and is dismissed relatively quickly. (GnomeDialog is really meant for transient dialogs.) Some "dialogs" are just small windows, such as the tool palette in the Gimp. These persistent ("floating") dialogs should be minimizable without minimizing the parent, and they should not be forced to stay above the parent window.

Your dialog should have a sensible default button---this is the button activated when the user presses the Enter key. gnome_dialog_set_default() specifies the default button. It's a judgment call which button should be the default. Often the best choice is the least-destructive action (i.e., "Cancel" rather than "OK"), but if neither is destructive, user convenience might guide your decision.

Typically, operations such as deleting data or quitting an application have "Cancel" or "No" as the default; dialogs that ask the user to enter text or other information typically have "OK" as the default. Remember that many window managers will focus windows when they pop up, so keystrokes users intend to go to their current application might go to your dialog instead. If your dialog has "delete all my files" as the default button, you will get hate mail.

Editable widgets emit the "activate" signal when Enter is pressed. Typically users expect Enter to activate the default dialog button, but if you have an editable widget such as GtkEntry in your dialog, it will capture any Enter presses, and keep the dialog's buttons from responding to them. gnome_dialog_editable_enters() activates the dialog's default button when the GtkEditable is activated, solving the problem.

gnome_dialog_set_sensitive() calls gtk_widget_set_sensitive() on button. If clicking a button makes no sense at a given time it should be desensitized.

Finally, you should make sure you do not create multiple instances of a dialog. Many applications allow you to pop up multiple Preferences or About dialogs; users will not trigger this bug very often, but it is a nice touch to avoid the problem. The following code deals with it in a simple way (note that the details of creating and showing the dialog have been omitted).


void 
do_dialog()
{
  static GtkWidget* dialog = NULL;

  if (dialog != NULL) 
    {
      /* This code tries to de-iconify and raise the dialog. 
       * It assumes the dialog is realized; if you can't 
       * ensure that, check that dialog->window != NULL.
       */

      gdk_window_show(dialog->window);
      gdk_window_raise(dialog->window);
    }
  else
    {
      dialog = gnome_dialog_new();      /* Arguments elided. */

      gtk_signal_connect(GTK_OBJECT(dialog),
                         "destroy",
                         GTK_SIGNAL_FUNC(gtk_widget_destroyed),
                         &dialog);

      /* Show the dialog, connect callbacks, etc. here */                   
    }
}
      

gtk_widget_destroyed() is defined in gtk/gtkwidget.h, and simply assigns NULL to its second argument. The code resets the dialog variable each time the user closes the dialog, and raises/deiconifies the dialog if the user tries to open it while another one is active. Note that the window manager has some say in whether the raise/deiconify is successful, so it is not guaranteed to happen.