LinuxQuestions.org
Visit Jeremy's Blog.
Go Back   LinuxQuestions.org > Articles > Technical
User Name
Password

Notices


By AndrewKrause at 2007-06-09 11:25
This article is the first part of a series that will introduce you to widgets and APIs that were recently introduced into GTK+. The tutorial is taken from Chapter 5 of Foundations of GTK+ Development (published April 2007). You can find more information about the book at http://www.gtkbook.com.





Today, we will cover the GtkAssistant widget, which was added in GTK+ 2.10. It allows you to create "Wizard" dialogs that span multiple pages.

Dialogs with Multiple Pages

With the release of GTK+ 2.10, a widget called GtkAssistant was introduced, which makes it easier to create dialogs with multiple stages, because you do not have to programmatically create the whole dialog. This allows you to split otherwise complex dialogs, into steps that guide the user. This functionality is implemented in what are often referred to as wizards throughout various applications.

This example begins by giving the user general information. The next page will not allow the user to proceed until text is entered in a GtkEntry widget. The third page will not allow the user to proceed until a GtkCheckButton button is activated. The fourth page will not let you do anything until the progress bar is filled, and the last page gives a summary of what has happened. This is the general flow that every GtkAssistant widget should follow.

Listing 5-11. The GtkAssistant Widget (assistant.c)
Code:
#include <gtk/gtk.h>
#include <string.h>

static void entry_changed    (GtkEditable*, GtkAssistant*);
static void button_toggled   (GtkCheckButton*, GtkAssistant*);
static void button_clicked   (GtkButton*, GtkAssistant*);
static void assistant_cancel (GtkAssistant*, gpointer);
static void assistant_close  (GtkAssistant*, gpointer);

typedef struct {
  GtkWidget *widget;
  gint index;
  const gchar *title;
  GtkAssistantPageType type;
  gboolean complete;
} PageInfo;

int main (int argc,
          char *argv[])
{
  GtkWidget *assistant, *entry, *label, *button, *progress, *hbox;
  guint i;
  PageInfo page[5] = {
    { NULL, -1, "Introduction",           GTK_ASSISTANT_PAGE_INTRO,    TRUE},
    { NULL, -1, NULL,                     GTK_ASSISTANT_PAGE_CONTENT,  FALSE},
    { NULL, -1, "Click the Check Button", GTK_ASSISTANT_PAGE_CONTENT,  FALSE},
    { NULL, -1, "Click the Button",       GTK_ASSISTANT_PAGE_PROGRESS, FALSE},
    { NULL, -1, "Confirmation",           GTK_ASSISTANT_PAGE_CONFIRM,  TRUE},
  };
  
  gtk_init (&argc, &argv);
   
  /* Create a new assistant widget with no pages. */
  assistant = gtk_assistant_new ();
  gtk_widget_set_size_request (assistant, 450, 300);
  gtk_window_set_title (GTK_WINDOW (assistant), "GtkAssistant Example");
  g_signal_connect (G_OBJECT (assistant), "destroy",
                    G_CALLBACK (gtk_main_quit), NULL);
  
  page[0].widget = gtk_label_new ("This is an example of a GtkAssistant. By\n"\
                                  "clicking the forward button, you can continue\n"\
                                  "to the next section!");
  page[1].widget = gtk_hbox_new (FALSE, 5);
  page[2].widget = gtk_check_button_new_with_label ("Click Me To Continue!");
  page[3].widget = gtk_alignment_new (0.5, 0.5, 0.0, 0.0);
  page[4].widget = gtk_label_new ("Text has been entered in the label and the\n"\
                                  "combo box is clicked. If you are done, then\n"\
                                  "it is time to leave!");
  
  /* Create the necessary widgets for the second page. */
  label = gtk_label_new ("Your Name: ");
  entry = gtk_entry_new ();
  gtk_box_pack_start (GTK_BOX (page[1].widget), label, FALSE, FALSE, 5);
  gtk_box_pack_start (GTK_BOX (page[1].widget), entry, FALSE, FALSE, 5);
  
  /* Create the necessary widgets for the fourth page. The, Attach the progress bar
   * to the GtkAlignment widget for later access.*/
  button = gtk_button_new_with_label ("Click me!");
  progress = gtk_progress_bar_new ();
  hbox = gtk_hbox_new (FALSE, 5);
  gtk_box_pack_start (GTK_BOX (hbox), progress, TRUE, FALSE, 5);
  gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, FALSE, 5);
  gtk_container_add (GTK_CONTAINER (page[3].widget), hbox);
  g_object_set_data (G_OBJECT (page[3].widget), "pbar", (gpointer) progress);
  
  /* Add five pages to the GtkAssistant dialog. */
  for (i = 0; i < 5; i++)
  {
    page[i].index = gtk_assistant_append_page (GTK_ASSISTANT (assistant),
                                               page[i].widget);
    gtk_assistant_set_page_title (GTK_ASSISTANT (assistant),
                                  page[i].widget, page[i].title);
    gtk_assistant_set_page_type (GTK_ASSISTANT (assistant),
                                  page[i].widget, page[i].type);

    /* Set the introduction and conclusion pages as complete so they can be
     * incremented or closed. */
    gtk_assistant_set_page_complete (GTK_ASSISTANT (assistant),
                                     page[i].widget, page[i].complete);
  }
  
  /* Update whether pages 2 through 4 are complete based upon whether there is
   * text in the GtkEntry, the check button is active, or the progress bar
   * is completely filled. */
  g_signal_connect (G_OBJECT (entry), "changed",
                    G_CALLBACK (entry_changed), (gpointer) assistant);
  g_signal_connect (G_OBJECT (page[2].widget), "toggled",
                    G_CALLBACK (button_toggled), (gpointer) assistant);
  g_signal_connect (G_OBJECT (button), "clicked",
                    G_CALLBACK (button_clicked), (gpointer) assistant);
  g_signal_connect (G_OBJECT (assistant), "cancel",
                    G_CALLBACK (assistant_cancel), NULL);
  g_signal_connect (G_OBJECT (assistant), "close",
                    G_CALLBACK (assistant_close), NULL);
  gtk_widget_show_all (assistant);
  gtk_main ();
  return 0;
}

/* If there is text in the GtkEntry, set the page as complete. Otherwise,
 * stop the user from progressing the next page. */
static void
entry_changed (GtkEditable *entry,
               GtkAssistant *assistant)
{
  const gchar *text = gtk_entry_get_text (GTK_ENTRY (entry));
  gint num = gtk_assistant_get_current_page (assistant);
  GtkWidget *page = gtk_assistant_get_nth_page (assistant, num);
  gtk_assistant_set_page_complete (assistant, page, (strlen (text) > 0));
}

/* If the check button is toggled, set the page as complete. Otherwise,
 * stop the user from progressing the next page. */
static void
button_toggled (GtkCheckButton *toggle,
                GtkAssistant *assistant)
{
  gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (toggle));
  gtk_assistant_set_page_complete (assistant, GTK_WIDGET (toggle), active);
}

/* Fill up the progress bar, 10% every second when the button is clicked. Then,
 * set the page as complete when the progress bar is filled. */
static void
button_clicked (GtkButton *button,
                GtkAssistant *assistant)
{
  GtkProgressBar *progress;
  GtkWidget *page;
  gdouble percent = 0.0;
  
  gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE);
  page = gtk_assistant_get_nth_page (assistant, 3);
  progress = GTK_PROGRESS_BAR (g_object_get_data (G_OBJECT (page), "pbar"));
  
  while (percent <= 100.0)
  {
    gchar *message = g_strdup_printf ("%.0f%% Complete", percent);
    gtk_progress_bar_set_fraction (progress, percent / 100.0);
    gtk_progress_bar_set_text (progress, message);
    
    while (gtk_events_pending ())
      gtk_main_iteration ();
      
    g_usleep (500000);
    percent += 5.0;
  }
  
  gtk_assistant_set_page_complete (assistant, page, TRUE);
}

/* If the dialog is cancelled, delete it from memory and then clean up after
 * the Assistant structure. */
static void
assistant_cancel (GtkAssistant *assistant,
                  gpointer data)
{
  gtk_widget_destroy (GTK_WIDGET (assistant));
}

/* This function is where you would apply the changes and destroy the assistant. */
static void
assistant_close (GtkAssistant *assistant,
                 gpointer data)
{
  g_print ("You would apply your changes now!\n");
  gtk_widget_destroy (GTK_WIDGET (assistant));
}
Creating GtkAssistant Pages

A GtkAssistant widget is a dialog with multiple pages, although it is actually not derived from GtkDialog. By calling gtk_assistant_new(), you create a new GtkAssistant widget with no initial pages.

index = gtk_assistant_append_page (GTK_ASSISTANT (assistant), widget);

There is no actual page widget for assistants, because each page is actually a child widget that is added with gtk_assistant_prepend_page(), gtk_assistant_append_page(), or gtk_assistant_insert_page(). Each of these functions accepts the child widget that is added as the content of the page and returns the new page’s index. Each page has a number of properties that can be set, each of which is optional. A list of these options follows:
  • Page title: Every page should have a title, so the user knows what it is for. Your first page should be an introductory page that tells the user information about the assistant. The last page must be a summary or confirmation page that makes sure the user is ready to apply the previous changes.
  • Header image: In the top panel, you can display an optional image to the left of the title. This is often the application’s logo or an image that complements the assistant’s purpose.
  • Side image: This optional image is placed along the left side of the assistant beside the main page content. It is meant to be used for aesthetic appeal.
  • Page type: The page type must always be set, or it will default to GTK_ASSISTANT_PAGE_CONTENT. The last page must always be a confirmation or summary page. You should also make the first page an introductory page that gives the user information about what task the assistant performs.

After you have set the page’s properties, you must choose what type of page it is. There are five types of pages. The first page should always be GTK_ASSISTANT_PAGE_INTRO. The last page should always be GTK_ASSISTANT_PAGE_CONFIRM or GTK_ASSISTANT_PAGE_SUMMARY—if your assistant does not end with one of those two types of pages, it will not work correctly. All of the available page types can be viewed in the following list:
  • GTK_ASSISTANT_PAGE_CONTENT: This type of page has general content, which means it will be used for almost every page in the assistant. It should never be used for the last page in an assistant.
  • GTK_ASSISTANT_PAGE_INTRO: This type of page has introductory information for the user. This should only be set for the first page in the assistant. While not required, introductory pages give the user direction and should be used in most assistants.
  • GTK_ASSISTANT_PAGE_CONFIRM: The page allows the user to confirm or deny a set of changes. This is usually used for changes that cannot be undone or may cause something to break if not set correctly. This should only be set for the last page of the assistant.
  • GTK_ASSISTANT_PAGE_SUMMARY: The page gives a summary of the changes that have occurred. This should only be set for the last page of the assistant.
  • GTK_ASSISTANT_PAGE_PROGRESS: When a task takes a long time to complete, this will block the assistant until the page is marked as complete. The difference between this page and a normal content page is that all of the buttons are disabled and the user is prevented from closing the assistant.

Caution: If you do not set the last page type as GTK_ASSISTANT_PAGE_CONFIRM or GTK_ASSISTANT_PAGE_SUMMARY, your application will abort with a GTK+ error when computing the last button state.

Since GtkAssistant is not derived from GtkDialog, you cannot use gtk_dialog_run() (or any other GtkDialog function) on this widget. Instead, the following four signals are provided for you to handle button clicked signals:
  • apply: This signal is emitted when the Apply button or Forward button is clicked on any assistant page.
  • cancel: This signal is emitted when the Cancel button is clicked on any assistant page.
  • close: This signal is emitted when the Close button or Apply button on the last page in the assistant is clicked.
  • prepare: Before making a new page visible, this signal is emitted so that you can do any preparation work before it is visible to the user.

You can connect to all GtkAssistant signals with g_signal_connect() or any other signal connection function provided by GLib. Excluding prepare, the callback functions for GtkAssistant signals receive the assistant and the user data parameter. The callback function for the prepare signal also accepts the child widget of the current page.

By default, every page is set as incomplete. You have to manually set each page as complete when the time is right with gtk_assistant_set_page_complete() or the GtkAssistant will not be able to progress to the next page.

void gtk_assistant_set_page_complete (GtkAssistant *assistant, GtkWidget *page, gboolean complete);

On every page, a Cancel button is displayed in addition to a few others. On pages other than the first one, a Back button is displayed that is always sensitive. This allows you to visit the previously displayed page and make changes.

Note: The page that is visited when the user clicks the Back button is not always the previous page according to the page index. It is the previously displayed page, which may be different based on how you defined the page flow of your assistant.

On every page except the last page, a Forward button is placed, which allows the user to move to the next page. On the last page an Apply button is displayed that allows the user to apply the changes. However, until the page is set as complete, the assistant will set the Forward or Apply button as insensitive. This allows you to prevent the user from proceeding until some action is taken.

In Listing 5-11, the first and last pages of the assistant were set as complete, because they were merely informative pages. This is the case in most assistants since they should begin with an introduction page and end with a confirmation or summary page.

The other two pages are where it becomes interesting. On the second page, we want to make sure that the user cannot proceed until text is entered in the GtkEntry widget. It would seem that that we should just check when text has been inserted and be done with it.

However, what happens if the user deletes all of the text? In this case, the forward button should be disabled yet again. To handle both of these actions, you can use GtkEditable’s changed signal. This will allow you to check the current state of the text in the entry upon every change, as in Listing 5-11.

On the third page, we want to enable the forward button only when the check button is active. To do this, we used the toggled signal of GtkToggleButton to check the current state of the check button. Based on this state, the forward button’s sensitivity was set.

The fourth page has a type of GTK_ASSISTANT_PAGE_PROGRESS, which disables all actions until the page is set as complete. The user is instructed to click a button, which begins the process of filling a GtkProgressBar widget 10 percent every second. When the progress bar is filled, the page is set as complete.

Page Forward Functions
There are times that you may want to skip to specific assistant pages if conditions are correct. For example, let us assume your application is creating a new project. Depending on the chosen language, you want to jump to either the third or fourth page. In this case, you will want to define your own GtkAssistantPageFunc function for forward motion.

You can use gtk_assistant_set_forward_page_func() to define a new page forward function for the assistant. By default, GTK+ will increment directly through the pages in order, one page at a time. By defining a new forward function, you can define the flow.

void gtk_assistant_set_forward_page_func (GtkAssistant *assistant, GtkAssistantPageFunc page_func, gpointer data, GDestroyNotify destroy_func);

For example, assistant_forward() is a simple GtkAssistantPageFunc implementation that moves from page two to either three or four depending on the condition returned by decide_next_page().

Code:
static gint
assistant_forward (gint current_page,
                   gpointer data)
{
  gint next_page = 0;
  switch (current_page)
  {
    case 0:
      next_page = 1;
      break;
    case 1:
      next_page = (decide_next_page() ? 2 : 3);
      break;
    case 2:
    case 3:
      next_page = 4;
      break;
    default:
      next_page = -1;
    }
  
  return next_page;
}
Note: By returning -1 from a page forward function, the user will be presented with a critical error and the assistant will not move to another page. The critical error message will tell the user that the page flow is broken.

In the assistant_forward() function, flow is changed based on the Boolean value returned by the fictional function decide_next_page(). In either case, the last page will be page 4. If the current page is not within bounds, -1 is returned, so an exception is thrown by GTK+.

While this GtkAssistant example is very simple, implementations of this widget can become very complex as they expand in number of pages. This widget could be re-created with a dialog, a GtkNotebook with hidden tabs, and a few buttons (I have had to do that very thing multiple times!), but it makes the process a lot easier.


  



All times are GMT -5. The time now is 09:49 AM.

Main Menu
Advertisement
Advertisement
My LQ
Write for LQ
LinuxQuestions.org is looking for people interested in writing Editorials, Articles, Reviews, and more. If you'd like to contribute content, let us know.
Main Menu
Syndicate
RSS1  Latest Threads
RSS1  LQ News
Twitter: @linuxquestions
Open Source Consulting | Domain Registration