GTK+ By Example/Tree View/Events

Selections, Double-Clicks and Context Menus

edit

Handling Selections

edit

One of the most basic features of a list or tree view is that rows can be selected or unselected. Selections are handled using the GtkTreeSelection object of a tree view. Every tree view automatically has a GtkTreeSelection associated with it, and you can get it using gtk_tree_view_get_selection. Selections are handled completely on the tree view side, which means that the model knows nothing about which rows are selected or not. There is no particular reason why selection handling could not have been implemented with functions that access the tree view widget directly, but for reasons of API cleanliness and code clarity the Gtk+ developers decided to create this special GtkTreeSelection object that then internally deals with the tree view widget. You will never need to create a tree selection object, it will be created for you automatically when you create a new tree view. You only need to use said gtk_tree_view_get_selection function to get a pointer to the selection object.

There are three ways to deal with tree view selections: either you get a list of the currently selected rows whenever you need it, for example within a context menu function, or you keep track of all select and unselect actions and keep a list of the currently selected rows around for whenever you need them; as a last resort, you can also traverse your list or tree and check each single row for whether it is selected or not (which you need to do if you want all rows that are not selected for example).

Selection Modes

edit

You can use gtk_tree_selection_set_mode to influence the way that selections are handled. There are four selection modes:

  • GTK_SELECTION_NONE - no items can be selected
  • GTK_SELECTION_SINGLE - no more than one item can be selected
  • GTK_SELECTION_BROWSE - exactly one item is always selected
  • GTK_SELECTION_MULTIPLE - anything between no item and all items can be selected

Getting the Currently Selected Rows

edit

You can access the currently selected rows either by traversing all selected rows using gtk_tree_selection_selected_foreach or get a GList of tree paths of the selected rows using gtk_tree_selection_get_selected_rows. Note that this function is only available in Gtk+-2.2 and newer, which means that you can't use it or need to reimplement it if you want your application to work with older installations.

If the selection mode you are using is either GTK_SELECTION_SINGLE or GTK_SELECTION_BROWSE, the most convenient way to get the selected row is the function gtk_tree_selection_get_selected, which will return TRUE and fill in the specified tree iter with the selected row (if a row is selected), and return FALSE otherwise. It is used like this:

  ...

  GtkTreeSelection *selection;
  GtkTreeModel     *model;
  GtkTreeIter       iter;

  /* This will only work in single or browse selection mode! */

  selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
  if (gtk_tree_selection_get_selected(selection, &model, &iter))
  {
    gchar *name;

    gtk_tree_model_get (model, &iter, COL_NAME, &name, -1);

    g_print ("selected row is: %s\n", name);

    g_free(name);
  }
  else
  {
    g_print ("no row selected.\n");
  }

  ...

One thing you need to be aware of is that you need to take care when removing rows from the model in a gtk_tree_selection_selected_foreach callback, or when looping through the list that gtk_tree_selection_get_selected_rows returns (because it contains paths, and when you remove rows in the middle, then the old paths will point to either a non-existing row, or to another row than the one selected). You have two ways around this problem: one way is to use the solution to removing multiple rows that has been described above, i.e. to get tree row references for all selected rows and then remove the rows one by one; the other solution is to sort the list of selected tree paths so that the last rows come first in the list, so that you remove rows from the end of the list or tree. You cannot remove rows from within a foreach callback in any case, that is simply not allowed.

Here is an example of how to use gtk_tree_selection_selected_foreach:

  ...

  gboolean
  view_selected_foreach_func (GtkTreeModel  *model,
                              GtkTreePath   *path,
                              GtkTreeIter   *iter,
                              gpointer       userdata)
  {
    gchar *name;

    gtk_tree_model_get (model, iter, COL_NAME, &name, -1);

    g_print ("%s is selected\n", name);
  }


  void
  do_something_with_all_selected_rows (GtkWidget *treeview)
  {
    GtkTreeSelection  *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));

    gtk_tree_selection_selected_foreach(selection, view_selected_foreach_func, NULL);
  }


  void
  create_view (void)
  {
    GtkWidget         *view;
    GtkTreeSelection  *selection;

    ...

    view = gtk_tree_view_new();

    ...

    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));

    gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
    ...
  }

  ...

Using Selection Functions

edit

You can set up a custom selection function with gtk_tree_selection_set_select_function. This function will then be called every time a row is going to be selected or unselected (meaning: it will be called before the selection status of that row is changed). Selection functions are commonly used for the following things:

  1. ... to keep track of the currently selected items (then you maintain a list of selected items yourself). In this case, note again that your selection function is called before the row's selection status is changed. In other words: if the row is going to be selected, then the boolean path_currently_selected variable that is passed to the selection function is still FALSE. Also note that the selection function might not always be called when a row is removed, so you either have to unselect a row before you remove it to make sure your selection function is called and removes the row from your list, or check the validity of a row when you process the selection list you keep. You should not store tree paths in your self-maintained list of selected rows, because whenever rows are added or removed or the model is resorted the paths might point to other rows. Use tree row references or other unique means of identifying a row instead.
  2. ... to tell Gtk+ whether it is allowed to select or unselect that specific row (you should make sure though that it is otherwise obvious to a user whether a row can be selected or not, otherwise the user will be confused if she just cannot select or unselect a row). This is done by returning TRUE or FALSE in the selection function.
  3. ... to take additional action whenever a row is selected or unselected.

Yet another simple example:

  ...

  gboolean
  view_selection_func (GtkTreeSelection *selection,
                       GtkTreeModel     *model,
                       GtkTreePath      *path,
                       gboolean          path_currently_selected,
                       gpointer          userdata)
  {
    GtkTreeIter iter;

    if (gtk_tree_model_get_iter(model, &iter, path))
    {
      gchar *name;

      gtk_tree_model_get(model, &iter, COL_NAME, &name, -1);

      if (!path_currently_selected)
      {
        g_print ("%s is going to be selected.\n", name);
      }
      else
      {
        g_print ("%s is going to be unselected.\n", name);
      }

      g_free(name);
    }

    return TRUE; /* allow selection state to change */
  }


  void
  create_view (void)
  {
    GtkWidget         *view;
    GtkTreeSelection  *selection;

    ...

    view = gtk_tree_view_new();

    ...

    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));

    gtk_tree_selection_set_select_function(selection, view_selection_func, NULL, NULL);
    ...
  }

  ...

Checking Whether a Row is Selected

edit

You can check whether a given row is selected or not using the functions gtk_tree_selection_iter_is_selected. or gtk_tree_selection_path_is_selected. If you want to know all rows that are not selected, for example, you could just traverse the whole list or tree, and use the above functions to check for each row whether it is selected or not.

Selecting and Unselecting Rows

edit

You can select or unselect rows manually with gtk_tree_selection_select_iter, gtk_tree_selection_select_path, gtk_tree_selection_unselect_iter, gtk_tree_selection_unselect_path, gtk_tree_selection_select_all, and gtk_tree_selection_unselect_all should you ever need to do that.

Getting the Number of Selected Rows

edit

Sometimes you want to know the number of rows that are currently selected (for example to set context menu entries active or inactive before you pop up a context menu). If you are using selection mode GTK_SELECTION_SINGLE or GTK_SELECTION_BROWSE, this is trivial to check with gtk_tree_selection_get_selected, which will return either TRUE or FALSE (meaning one selected row or no selected row).

If you are using GTK_SELECTION_MULTIPLE or want a more general approach that works for all selection modes, gtk_tree_selection_count_selected_rows will return the information you are looking for. The only caveat with this function is that it only exists in Gtk+-2.2 and newer, so you will have to reimplement it if you want users with old installations that still use Gtk+-2.0 to be able to use your program as well. Here is a way to reimplement this function:

  static void
  count_foreach_helper (GtkTreeModel *model,
                        GtkTreePath  *path,
                        GtkTreeIter  *iter,
                        gpointer      userdata)
  {
    gint *p_count = (gint*) userdata;

    g_assert (p_count != NULL);

    *p_count = *p_count + 1;
  }

  gint
  my_tree_selection_count_selected_rows (GtkTreeSelection *selection)
  {
    gint count = 0;

    gtk_tree_selection_selected_foreach(selection, count_foreach_helper, &count);

    return count;
  }

Double-Clicks on a Row

edit

Catching double-clicks on a row is quite easy and is done by connecting to a tree view's "row-activated" signal, like this:

  void
  view_onRowActivated (GtkTreeView        *treeview,
                       GtkTreePath        *path,
                       GtkTreeViewColumn  *col,
                       gpointer            userdata)
  {
    GtkTreeModel *model;
    GtkTreeIter   iter;

    g_print ("A row has been double-clicked!\n");

    model = gtk_tree_view_get_model(treeview);

    if (gtk_tree_model_get_iter(model, &iter, path))
    {
       gchar *name;

       gtk_tree_model_get(model, &iter, COLUMN_NAME, &name, -1);

       g_print ("Double-clicked row contains name %s\n", name);

       g_free(name);
    }
  }

  void
  create_view (void)
  {
    GtkWidget *view;

    view = gtk_tree_view_new();

    ...

    g_signal_connect(view, "row-activated", (GCallback) view_onRowActivated, NULL);

    ...
  }

Context Menus on Right Click

edit

Context menus are context-dependent menus that pop up when a user right-clicks on a list or tree and usually let the user do something with the selected items or manipulate the list or tree in other ways.

Right-clicks on a tree view are caught just like mouse button clicks are caught with any other widgets, namely by connecting to the tree view's "button_press_event" signal handler (which is a GtkWidget signal, and as GtkTreeView is derived from GtkWidget it has this signal as well). Additionally, you should also connect to the "popup-menu" signal, so users can access your context menu without a mouse. The "popup-menu" signal is emitted when the user presses Shift-F10. Also, you should make sure that all functions provided in your context menu can also be accessed by other means such as the application's main menu. See the GNOME Human Interface Guidelines (HIG) for more details. Straight from the a-snippet-of-code-says-more-than-a-thousand-words-department, some code to look at:

  void
  view_popup_menu_onDoSomething (GtkWidget *menuitem, gpointer userdata)
  {
    /* we passed the view as userdata when we connected the signal */
    GtkTreeView *treeview = GTK_TREE_VIEW(userdata);

    g_print ("Do something!\n");
  }


  void
  view_popup_menu (GtkWidget *treeview, GdkEventButton *event, gpointer userdata)
  {
    GtkWidget *menu, *menuitem;

    menu = gtk_menu_new();

    menuitem = gtk_menu_item_new_with_label("Do something");

    g_signal_connect(menuitem, "activate",
                     (GCallback) view_popup_menu_onDoSomething, treeview);

    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);

    gtk_widget_show_all(menu);

    /* Note: event can be NULL here when called from view_onPopupMenu;
     *  gdk_event_get_time() accepts a NULL argument */
    gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
                   (event != NULL) ? event->button : 0,
                   gdk_event_get_time((GdkEvent*)event));
  }


  gboolean
  view_onButtonPressed (GtkWidget *treeview, GdkEventButton *event, gpointer userdata)
  {
    /* single click with the right mouse button? */
    if (event->type == GDK_BUTTON_PRESS  &&  event->button == 3)
    {
      g_print ("Single right click on the tree view.\n");

      /* optional: select row if no row is selected or only
       *  one other row is selected (will only do something
       *  if you set a tree selection mode as described later
       *  in the tutorial) */
      if (1)
      {
        GtkTreeSelection *selection;

        selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));

        /* Note: gtk_tree_selection_count_selected_rows() does not
         *   exist in gtk+-2.0, only in gtk+ >= v2.2 ! */
        if (gtk_tree_selection_count_selected_rows(selection)  <= 1)
        {
           GtkTreePath *path;

           /* Get tree path for row that was clicked */
           if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeview),
                                             (gint) event->x, 
                                             (gint) event->y,
                                             &path, NULL, NULL, NULL))
           {
             gtk_tree_selection_unselect_all(selection);
             gtk_tree_selection_select_path(selection, path);
             gtk_tree_path_free(path);
           }
        }
      } /* end of optional bit */

      view_popup_menu(treeview, event, userdata);

      return TRUE; /* we handled this */
    }

    return FALSE; /* we did not handle this */
  }


  gboolean
  view_onPopupMenu (GtkWidget *treeview, gpointer userdata)
  {
    view_popup_menu(treeview, NULL, userdata);

    return TRUE; /* we handled this */
  }


  void
  create_view (void)
  {
    GtkWidget *view;

    view = gtk_tree_view_new();

    ...

    g_signal_connect(view, "button-press-event", (GCallback) view_onButtonPressed, NULL);
    g_signal_connect(view, "popup-menu", (GCallback) view_onPopupMenu, NULL);

    ...
  }