453 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			453 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| #include <gtk/gtk.h>
 | |
| 
 | |
| /* TestItem {{{1 */
 | |
| 
 | |
| /* This utility struct is used by both the RandomMenu and MirrorMenu
 | |
|  * class implementations below.
 | |
|  */
 | |
| typedef struct {
 | |
|   GHashTable *attributes;
 | |
|   GHashTable *links;
 | |
| } TestItem;
 | |
| 
 | |
| static TestItem *
 | |
| test_item_new (GHashTable *attributes,
 | |
|                GHashTable *links)
 | |
| {
 | |
|   TestItem *item;
 | |
| 
 | |
|   item = g_slice_new (TestItem);
 | |
|   item->attributes = g_hash_table_ref (attributes);
 | |
|   item->links = g_hash_table_ref (links);
 | |
| 
 | |
|   return item;
 | |
| }
 | |
| 
 | |
| static void
 | |
| test_item_free (gpointer data)
 | |
| {
 | |
|   TestItem *item = data;
 | |
| 
 | |
|   g_hash_table_unref (item->attributes);
 | |
|   g_hash_table_unref (item->links);
 | |
| 
 | |
|   g_slice_free (TestItem, item);
 | |
| }
 | |
| 
 | |
| /* RandomMenu {{{1 */
 | |
| #define MAX_ITEMS 10
 | |
| #define TOP_ORDER 4
 | |
| 
 | |
| typedef struct {
 | |
|   GMenuModel parent_instance;
 | |
| 
 | |
|   GSequence *items;
 | |
|   gint order;
 | |
| } RandomMenu;
 | |
| 
 | |
| typedef GMenuModelClass RandomMenuClass;
 | |
| 
 | |
| static GType random_menu_get_type (void);
 | |
| G_DEFINE_TYPE (RandomMenu, random_menu, G_TYPE_MENU_MODEL);
 | |
| 
 | |
| static gboolean
 | |
| random_menu_is_mutable (GMenuModel *model)
 | |
| {
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| static gint
 | |
| random_menu_get_n_items (GMenuModel *model)
 | |
| {
 | |
|   RandomMenu *menu = (RandomMenu *) model;
 | |
| 
 | |
|   return g_sequence_get_length (menu->items);
 | |
| }
 | |
| 
 | |
| static void
 | |
| random_menu_get_item_attributes (GMenuModel  *model,
 | |
|                                  gint         position,
 | |
|                                  GHashTable **table)
 | |
| {
 | |
|   RandomMenu *menu = (RandomMenu *) model;
 | |
|   TestItem *item;
 | |
| 
 | |
|   item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
 | |
|   *table = g_hash_table_ref (item->attributes);
 | |
| }
 | |
| 
 | |
| static void
 | |
| random_menu_get_item_links (GMenuModel  *model,
 | |
|                             gint         position,
 | |
|                             GHashTable **table)
 | |
| {
 | |
|   RandomMenu *menu = (RandomMenu *) model;
 | |
|   TestItem *item;
 | |
| 
 | |
|   item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
 | |
|   *table = g_hash_table_ref (item->links);
 | |
| }
 | |
| 
 | |
| static void
 | |
| random_menu_finalize (GObject *object)
 | |
| {
 | |
|   RandomMenu *menu = (RandomMenu *) object;
 | |
| 
 | |
|   g_sequence_free (menu->items);
 | |
| 
 | |
|   G_OBJECT_CLASS (random_menu_parent_class)
 | |
|     ->finalize (object);
 | |
| }
 | |
| 
 | |
| static void
 | |
| random_menu_init (RandomMenu *menu)
 | |
| {
 | |
| }
 | |
| 
 | |
| static void
 | |
| random_menu_class_init (GMenuModelClass *class)
 | |
| {
 | |
|   GObjectClass *object_class = G_OBJECT_CLASS (class);
 | |
| 
 | |
|   class->is_mutable = random_menu_is_mutable;
 | |
|   class->get_n_items = random_menu_get_n_items;
 | |
|   class->get_item_attributes = random_menu_get_item_attributes;
 | |
|   class->get_item_links = random_menu_get_item_links;
 | |
| 
 | |
|   object_class->finalize = random_menu_finalize;
 | |
| }
 | |
| 
 | |
| static RandomMenu * random_menu_new (GRand *rand, gint order);
 | |
| 
 | |
| static void
 | |
| random_menu_change (RandomMenu *menu,
 | |
|                     GRand      *rand)
 | |
| {
 | |
|   gint position, removes, adds;
 | |
|   GSequenceIter *point;
 | |
|   gint n_items;
 | |
|   gint i;
 | |
| 
 | |
|   n_items = g_sequence_get_length (menu->items);
 | |
| 
 | |
|   do
 | |
|     {
 | |
|       position = g_rand_int_range (rand, 0, n_items + 1);
 | |
|       removes = g_rand_int_range (rand, 0, n_items - position + 1);
 | |
|       adds = g_rand_int_range (rand, 0, MAX_ITEMS - (n_items - removes) + 1);
 | |
|     }
 | |
|   while (removes == 0 && adds == 0);
 | |
| 
 | |
|   point = g_sequence_get_iter_at_pos (menu->items, position + removes);
 | |
| 
 | |
|   if (removes)
 | |
|     {
 | |
|       GSequenceIter *start;
 | |
| 
 | |
|       start = g_sequence_get_iter_at_pos (menu->items, position);
 | |
|       g_sequence_remove_range (start, point);
 | |
|     }
 | |
| 
 | |
|   for (i = 0; i < adds; i++)
 | |
|     {
 | |
|       const gchar *label;
 | |
|       GHashTable *links;
 | |
|       GHashTable *attributes;
 | |
| 
 | |
|       attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
 | |
|       links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_object_unref);
 | |
| 
 | |
|       if (menu->order > 0 && g_rand_boolean (rand))
 | |
|         {
 | |
|           RandomMenu *child;
 | |
|           const gchar *subtype;
 | |
| 
 | |
|           child = random_menu_new (rand, menu->order - 1);
 | |
| 
 | |
|           if (g_rand_boolean (rand))
 | |
|             {
 | |
|               subtype = G_MENU_LINK_SECTION;
 | |
|               /* label some section headers */
 | |
|               if (g_rand_boolean (rand))
 | |
|                 label = "Section";
 | |
|               else
 | |
|                 label = NULL;
 | |
|             }
 | |
|           else
 | |
|             {
 | |
|               /* label all submenus */
 | |
|               subtype = G_MENU_LINK_SUBMENU;
 | |
|               label = "Submenu";
 | |
|             }
 | |
| 
 | |
|           g_hash_table_insert (links, g_strdup (subtype), child);
 | |
|         }
 | |
|       else
 | |
|         /* label all terminals */
 | |
|         label = "Menu Item";
 | |
| 
 | |
|       if (label)
 | |
|         g_hash_table_insert (attributes, g_strdup ("label"), g_variant_ref_sink (g_variant_new_string (label)));
 | |
| 
 | |
|       g_sequence_insert_before (point, test_item_new (attributes, links));
 | |
|       g_hash_table_unref (links);
 | |
|       g_hash_table_unref (attributes);
 | |
|     }
 | |
| 
 | |
|   g_menu_model_items_changed (G_MENU_MODEL (menu), position, removes, adds);
 | |
| }
 | |
| 
 | |
| static RandomMenu *
 | |
| random_menu_new (GRand *rand,
 | |
|                  gint   order)
 | |
| {
 | |
|   RandomMenu *menu;
 | |
| 
 | |
|   menu = g_object_new (random_menu_get_type (), NULL);
 | |
|   menu->items = g_sequence_new (test_item_free);
 | |
|   menu->order = order;
 | |
| 
 | |
|   random_menu_change (menu, rand);
 | |
| 
 | |
|   return menu;
 | |
| }
 | |
| 
 | |
| /* Test cases {{{1 */
 | |
| 
 | |
| static void assert_menu_equality (GtkContainer *container, GMenuModel   *model);
 | |
| 
 | |
| static const gchar *
 | |
| get_label (GtkMenuItem *item)
 | |
| {
 | |
|   GList *children = gtk_container_get_children (GTK_CONTAINER (item));
 | |
|   const gchar *label = NULL;
 | |
| 
 | |
|   while (children)
 | |
|     {
 | |
|       if (GTK_IS_CONTAINER (children->data))
 | |
|         children = g_list_concat (children, gtk_container_get_children (children->data));
 | |
|       else if (GTK_IS_LABEL (children->data))
 | |
|         label = gtk_label_get_text (children->data);
 | |
| 
 | |
|       children = g_list_delete_link (children, children);
 | |
|     }
 | |
| 
 | |
|   return label;
 | |
| }
 | |
| 
 | |
| /* a bit complicated with the separators...
 | |
|  *
 | |
|  * with_separators are if subsections of this GMenuModel should have
 | |
|  * separators inserted between them (ie: in the same sense as the
 | |
|  * 'with_separators' argument to gtk_menu_shell_bind_model().
 | |
|  *
 | |
|  * needs_separator is true if this particular section needs to have a
 | |
|  * separator before it in the case that it is non-empty.  this will be
 | |
|  * defined for all subsections of a with_separators menu (except the
 | |
|  * first) or in case section_header is non-%NULL.
 | |
|  *
 | |
|  * section_header is the label that must be inside that separator, if it
 | |
|  * exists.  section_header is only non-%NULL if needs_separator is also
 | |
|  * TRUE.
 | |
|  */
 | |
| static void
 | |
| assert_section_equality (GSList      **children,
 | |
|                          gboolean      with_separators,
 | |
|                          gboolean      needs_separator,
 | |
|                          const gchar  *section_header,
 | |
|                          GMenuModel   *model)
 | |
| {
 | |
|   gboolean has_separator;
 | |
|   GSList *our_children;
 | |
|   gint i, n;
 | |
| 
 | |
|   /* Assuming that we have the possibility of showing a separator, there
 | |
|    * are two valid situations:
 | |
|    *
 | |
|    *  - we have a separator and we have other children
 | |
|    *
 | |
|    *  - we have no separator and no children
 | |
|    *
 | |
|    * If we see a separator, we suppose that it is ours and that we will
 | |
|    * encounter children.  In the case that we have no children, the
 | |
|    * separator may not be ours but may rather belong to a later section.
 | |
|    *
 | |
|    * We therefore keep our own copy of the children GSList.  If we
 | |
|    * encounter children, we will delete the links that this section is
 | |
|    * responsible for and update the pass-by-reference value.  Otherwise,
 | |
|    * we will leave everything alone and let the separator be accounted
 | |
|    * for by a following section.
 | |
|    */
 | |
|   our_children = *children;
 | |
|   if (needs_separator && GTK_IS_SEPARATOR_MENU_ITEM (our_children->data))
 | |
|     {
 | |
|        /* We accounted for the separator, at least for now, so remove it
 | |
|        * from the list.
 | |
|        *
 | |
|        * We will check later if we should have actually had a separator
 | |
|        * and compare the result to has_separator.
 | |
|        */
 | |
|       our_children = our_children->next;
 | |
|       has_separator = TRUE;
 | |
|     }
 | |
|   else
 | |
|     has_separator = FALSE;
 | |
| 
 | |
|   /* Now, iterate the model checking that the items in the GSList line
 | |
|    * up with our expectations. */
 | |
|   n = g_menu_model_get_n_items (model);
 | |
|   for (i = 0; i < n; i++)
 | |
|     {
 | |
|       GMenuModel *subsection;
 | |
|       GMenuModel *submenu;
 | |
|       gchar *label = NULL;
 | |
| 
 | |
|       subsection = g_menu_model_get_item_link (model, i, G_MENU_LINK_SECTION);
 | |
|       submenu = g_menu_model_get_item_link (model, i, G_MENU_LINK_SUBMENU);
 | |
|       g_menu_model_get_item_attribute (model, i, G_MENU_ATTRIBUTE_LABEL, "s", &label);
 | |
| 
 | |
|       if (subsection)
 | |
|         {
 | |
|           g_assert (!submenu);
 | |
|           assert_section_equality (&our_children,
 | |
|                                    FALSE,                                /* with_separators */
 | |
|                                    label || (with_separators && i > 0),  /* needs_separator */
 | |
|                                    label,                                /* section_header */
 | |
|                                    subsection);
 | |
|           g_object_unref (subsection);
 | |
|         }
 | |
|       else
 | |
|         {
 | |
|           GtkWidget *submenu_widget;
 | |
|           GtkMenuItem *item;
 | |
| 
 | |
|           /* This is a normal item.  Make sure the label is right. */
 | |
|           item = our_children->data;
 | |
|           our_children = g_slist_remove (our_children, item);
 | |
| 
 | |
|           /* get_label() returns "" when it ought to return NULL */
 | |
|           g_assert_cmpstr (get_label (item), ==, label ? label : "");
 | |
|           submenu_widget = gtk_menu_item_get_submenu (item);
 | |
| 
 | |
|           if (submenu)
 | |
|             {
 | |
|               g_assert (submenu_widget != NULL);
 | |
|               assert_menu_equality (GTK_CONTAINER (submenu_widget), submenu);
 | |
|               g_object_unref (submenu);
 | |
|             }
 | |
|           else
 | |
|             g_assert (!submenu_widget);
 | |
|         }
 | |
| 
 | |
|       g_free (label);
 | |
|     }
 | |
| 
 | |
|   /* If we found a separator but visited no children then the separator
 | |
|    * was not for us.  Patch that up.
 | |
|    */
 | |
|   if (has_separator && our_children == (*children)->next)
 | |
|     {
 | |
|       /* Rewind our_children to put the separator we tentatively
 | |
|        * consumed back into the list.
 | |
|        */
 | |
|       our_children = *children;
 | |
|       has_separator = FALSE;
 | |
|     }
 | |
| 
 | |
|   if (our_children == *children)
 | |
|     /* If we had no children then we didn't really need a separator. */
 | |
|     needs_separator = FALSE;
 | |
| 
 | |
|   g_assert (needs_separator == has_separator);
 | |
| 
 | |
|   if (has_separator)
 | |
|     {
 | |
|       GtkWidget *contents;
 | |
|       const gchar *label;
 | |
| 
 | |
|       /* We needed and had a separator and we visited a child.
 | |
|        *
 | |
|        * Make sure that separator was valid.
 | |
|        */
 | |
|       contents = gtk_bin_get_child ((*children)->data);
 | |
|       if (GTK_IS_LABEL (contents))
 | |
|         label = gtk_label_get_label (GTK_LABEL (contents));
 | |
|       else
 | |
|         label = "";
 | |
| 
 | |
|       /* get_label() returns "" when it ought to return NULL */
 | |
|       g_assert_cmpstr (label, ==, section_header ? section_header : "");
 | |
| 
 | |
|       /* our_children has already gone (possibly far) past *children, so
 | |
|        * we need to free up the link that we left behind for the
 | |
|        * separator in case we wanted to rewind.
 | |
|        */
 | |
|       g_slist_free_1 (*children);
 | |
|     }
 | |
| 
 | |
|   *children = our_children;
 | |
| }
 | |
| 
 | |
| /* We want to use a GSList here instead of a GList because the ->prev
 | |
|  * pointer updates cause trouble with the way we speculatively deal with
 | |
|  * separators by skipping over them and coming back to clean up later.
 | |
|  */
 | |
| static void
 | |
| get_children_into_slist (GtkWidget *widget,
 | |
|                          gpointer   user_data)
 | |
| {
 | |
|   GSList **list_ptr = user_data;
 | |
| 
 | |
|   *list_ptr = g_slist_prepend (*list_ptr, widget);
 | |
| }
 | |
| 
 | |
| static void
 | |
| assert_menu_equality (GtkContainer *container,
 | |
|                       GMenuModel   *model)
 | |
| {
 | |
|   GSList *children = NULL;
 | |
| 
 | |
|   gtk_container_foreach (container, get_children_into_slist, &children);
 | |
|   children = g_slist_reverse (children);
 | |
| 
 | |
|   assert_section_equality (&children, TRUE, FALSE, NULL, model);
 | |
|   g_assert (children == NULL);
 | |
| }
 | |
| 
 | |
| static void
 | |
| test_bind_menu (void)
 | |
| {
 | |
|   RandomMenu *model;
 | |
|   GtkWidget *menu;
 | |
|   GRand *rand;
 | |
|   gint i;
 | |
| 
 | |
|   gtk_init (0, 0);
 | |
| 
 | |
|   rand = g_rand_new_with_seed (g_test_rand_int ());
 | |
|   model = random_menu_new (rand, TOP_ORDER);
 | |
|   menu = gtk_menu_new_from_model (G_MENU_MODEL (model));
 | |
|   g_object_ref_sink (menu);
 | |
|   assert_menu_equality (GTK_CONTAINER (menu), G_MENU_MODEL (model));
 | |
|   for (i = 0; i < 100; i++)
 | |
|     {
 | |
|       random_menu_change (model, rand);
 | |
|       while (g_main_context_iteration (NULL, FALSE));
 | |
|       assert_menu_equality (GTK_CONTAINER (menu), G_MENU_MODEL (model));
 | |
|     }
 | |
|   g_object_unref (model);
 | |
|   g_object_unref (menu);
 | |
|   g_rand_free (rand);
 | |
| }
 | |
| /* Epilogue {{{1 */
 | |
| int
 | |
| main (int argc, char **argv)
 | |
| {
 | |
|   g_test_init (&argc, &argv, NULL);
 | |
| 
 | |
|   g_test_add_func ("/gmenu/bind", test_bind_menu);
 | |
| 
 | |
|   return g_test_run ();
 | |
| }
 | |
| /* vim:set foldmethod=marker: */
 | 
