]> Pileus Git - ~andy/gtk/blob - gtk/gtkmodelmenu.c
gtkmodelmenu: add support for action namespaces
[~andy/gtk] / gtk / gtkmodelmenu.c
1 /*
2  * Copyright © 2011 Red Hat, Inc.
3  * Copyright © 2011 Canonical Limited
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the licence, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
17  *
18  * Author: Matthias Clasen <mclasen@redhat.com>
19  *         Ryan Lortie <desrt@desrt.ca>
20  */
21
22 #include "config.h"
23
24 #include "gtkmodelmenu.h"
25
26 #include "gtkmenu.h"
27 #include "gtkmenubar.h"
28 #include "gtkseparatormenuitem.h"
29 #include "gtkmodelmenuitem.h"
30 #include "gtkapplicationprivate.h"
31
32 #define MODEL_MENU_WIDGET_DATA "gtk-model-menu-widget-data"
33
34 typedef struct {
35   GMenuModel        *model;
36   GtkAccelGroup     *accels;
37   GtkMenuShell      *shell;
38   guint              update_idle;
39   GSList            *connected;
40   gboolean           with_separators;
41   gint               n_items;
42   gchar             *action_namespace;
43 } GtkModelMenuBinding;
44
45 static void
46 gtk_model_menu_binding_items_changed (GMenuModel *model,
47                                       gint        position,
48                                       gint        removed,
49                                       gint        added,
50                                       gpointer    user_data);
51 static void gtk_model_menu_binding_append_model (GtkModelMenuBinding *binding,
52                                                  GMenuModel *model,
53                                                  const gchar *action_namespace,
54                                                  gboolean with_separators);
55
56 static void
57 gtk_model_menu_binding_free (gpointer data)
58 {
59   GtkModelMenuBinding *binding = data;
60
61   /* disconnect all existing signal handlers */
62   while (binding->connected)
63     {
64       g_signal_handlers_disconnect_by_func (binding->connected->data, gtk_model_menu_binding_items_changed, binding);
65       g_object_unref (binding->connected->data);
66
67       binding->connected = g_slist_delete_link (binding->connected, binding->connected);
68     }
69
70   g_object_unref (binding->model);
71   g_free (binding->action_namespace);
72
73   g_slice_free (GtkModelMenuBinding, binding);
74 }
75
76 static void
77 gtk_model_menu_binding_append_item (GtkModelMenuBinding  *binding,
78                                     GMenuModel           *model,
79                                     const gchar          *action_namespace,
80                                     gint                  item_index,
81                                     gchar               **heading)
82 {
83   GMenuModel *section;
84
85   if ((section = g_menu_model_get_item_link (model, item_index, "section")))
86     {
87       const gchar *section_namespace = NULL;
88
89       g_menu_model_get_item_attribute (model, item_index, "label", "s", heading);
90       g_menu_model_get_item_attribute (model, item_index, "action-namespace", "s", &section_namespace);
91
92       if (action_namespace)
93         {
94           gchar *namespace = g_strjoin (".", action_namespace, section_namespace, NULL);
95           gtk_model_menu_binding_append_model (binding, section, namespace, FALSE);
96           g_free (namespace);
97         }
98       else
99         {
100           gtk_model_menu_binding_append_model (binding, section, section_namespace, FALSE);
101         }
102
103       g_free (section_namespace);
104       g_object_unref (section);
105     }
106   else
107     {
108       GtkMenuItem *item;
109
110       item = gtk_model_menu_item_new (model, item_index, action_namespace, binding->accels);
111       gtk_menu_shell_append (binding->shell, GTK_WIDGET (item));
112       gtk_widget_show (GTK_WIDGET (item));
113       binding->n_items++;
114     }
115 }
116
117 static void
118 gtk_model_menu_binding_append_model (GtkModelMenuBinding *binding,
119                                      GMenuModel          *model,
120                                      const gchar         *action_namespace,
121                                      gboolean             with_separators)
122 {
123   gint n, i;
124
125   g_signal_connect (model, "items-changed", G_CALLBACK (gtk_model_menu_binding_items_changed), binding);
126   binding->connected = g_slist_prepend (binding->connected, g_object_ref (model));
127
128   /* Deciding if we should show a separator is a bit difficult.  There
129    * are two types of separators:
130    *
131    *  - section headings (when sections have 'label' property)
132    *
133    *  - normal separators automatically put between sections
134    *
135    * The easiest way to think about it is that a section usually has a
136    * separator (or heading) immediately before it.
137    *
138    * There are three exceptions to this general rule:
139    *
140    *  - empty sections don't get separators or headings
141    *
142    *  - sections only get separators and headings at the toplevel of a
143    *    menu (ie: no separators on nested sections or in menubars)
144    *
145    *  - the first section in the menu doesn't get a normal separator,
146    *    but it can get a header (if it's not empty)
147    *
148    * Unfortunately, we cannot simply check the size of the section in
149    * order to determine if we should place a header: the section may
150    * contain other sections that are themselves empty.  Instead, we need
151    * to append the section, and check if we ended up with any actual
152    * content.  If we did, then we need to insert before that content.
153    * We use 'our_position' to keep track of this.
154    */
155
156   n = g_menu_model_get_n_items (model);
157
158   for (i = 0; i < n; i++)
159     {
160       gint our_position = binding->n_items;
161       gchar *heading = NULL;
162
163       gtk_model_menu_binding_append_item (binding, model, action_namespace, i, &heading);
164
165       if (with_separators && our_position < binding->n_items)
166         {
167           GtkWidget *separator = NULL;
168
169           if (heading)
170             {
171               separator = gtk_menu_item_new_with_label (heading);
172               gtk_widget_set_sensitive (separator, FALSE);
173             }
174           else if (our_position > 0)
175             separator = gtk_separator_menu_item_new ();
176
177           if (separator)
178             {
179               gtk_menu_shell_insert (binding->shell, separator, our_position);
180               gtk_widget_show (separator);
181               binding->n_items++;
182             }
183         }
184
185       g_free (heading);
186     }
187 }
188
189 static void
190 gtk_model_menu_binding_populate (GtkModelMenuBinding *binding)
191 {
192   GList *children;
193
194   /* remove current children */
195   children = gtk_container_get_children (GTK_CONTAINER (binding->shell));
196   while (children)
197     {
198       gtk_container_remove (GTK_CONTAINER (binding->shell), children->data);
199       children = g_list_delete_link (children, children);
200     }
201
202   binding->n_items = 0;
203
204   /* add new items from the model */
205   gtk_model_menu_binding_append_model (binding, binding->model, binding->action_namespace, binding->with_separators);
206 }
207
208 static gboolean
209 gtk_model_menu_binding_handle_changes (gpointer user_data)
210 {
211   GtkModelMenuBinding *binding = user_data;
212
213   /* disconnect all existing signal handlers */
214   while (binding->connected)
215     {
216       g_signal_handlers_disconnect_by_func (binding->connected->data, gtk_model_menu_binding_items_changed, binding);
217       g_object_unref (binding->connected->data);
218
219       binding->connected = g_slist_delete_link (binding->connected, binding->connected);
220     }
221
222   gtk_model_menu_binding_populate (binding);
223
224   binding->update_idle = 0;
225
226   g_object_unref (binding->shell);
227
228   return G_SOURCE_REMOVE;
229 }
230
231 static void
232 gtk_model_menu_binding_items_changed (GMenuModel *model,
233                                       gint        position,
234                                       gint        removed,
235                                       gint        added,
236                                       gpointer    user_data)
237 {
238   GtkModelMenuBinding *binding = user_data;
239
240   if (binding->update_idle == 0)
241     {
242       binding->update_idle = gdk_threads_add_idle (gtk_model_menu_binding_handle_changes, user_data);
243       g_object_ref (binding->shell);
244     }
245 }
246
247 static void
248 gtk_model_menu_bind (GtkMenuShell *shell,
249                      GMenuModel   *model,
250                      const gchar  *action_namespace,
251                      gboolean      with_separators)
252 {
253   GtkModelMenuBinding *binding;
254
255   binding = g_slice_new (GtkModelMenuBinding);
256   binding->model = g_object_ref (model);
257   binding->accels = NULL;
258   binding->shell = shell;
259   binding->update_idle = 0;
260   binding->connected = NULL;
261   binding->with_separators = with_separators;
262   binding->action_namespace = g_strdup (action_namespace);
263
264   g_object_set_data_full (G_OBJECT (shell), "gtk-model-menu-binding", binding, gtk_model_menu_binding_free);
265 }
266
267
268 static void
269 gtk_model_menu_populate (GtkMenuShell  *shell,
270                          GtkAccelGroup *accels)
271 {
272   GtkModelMenuBinding *binding;
273
274   binding = (GtkModelMenuBinding*) g_object_get_data (G_OBJECT (shell), "gtk-model-menu-binding");
275
276   binding->accels = accels;
277
278   gtk_model_menu_binding_populate (binding);
279 }
280
281 GtkWidget *
282 gtk_model_menu_create_menu (GMenuModel    *model,
283                             const gchar   *action_namespace,
284                             GtkAccelGroup *accels)
285 {
286   GtkWidget *menu;
287
288   menu = gtk_menu_new ();
289   gtk_menu_set_accel_group (GTK_MENU (menu), accels);
290
291   gtk_model_menu_bind (GTK_MENU_SHELL (menu), model, action_namespace, TRUE);
292   gtk_model_menu_populate (GTK_MENU_SHELL (menu), accels);
293
294   return menu;
295 }
296
297 /**
298  * gtk_menu_new_from_model:
299  * @model: a #GMenuModel
300  *
301  * Creates a #GtkMenu and populates it with menu items and
302  * submenus according to @model.
303  *
304  * The created menu items are connected to actions found in the
305  * #GtkApplicationWindow to which the menu belongs - typically
306  * by means of being attached to a widget (see gtk_menu_attach_to_widget())
307  * that is contained within the #GtkApplicationWindows widget hierarchy.
308  *
309  * Returns: a new #GtkMenu
310  *
311  * Since: 3.4
312  */
313 GtkWidget *
314 gtk_menu_new_from_model (GMenuModel *model)
315 {
316   GtkWidget *menu;
317
318   menu = gtk_menu_new ();
319   gtk_model_menu_bind (GTK_MENU_SHELL (menu), model, NULL, TRUE);
320   gtk_model_menu_populate (GTK_MENU_SHELL (menu), NULL);
321
322   return menu;
323 }
324
325 GtkWidget *
326 gtk_model_menu_create_menu_bar (GMenuModel    *model,
327                                 GtkAccelGroup *accels)
328 {
329   GtkWidget *menubar;
330
331   menubar = gtk_menu_bar_new ();
332
333   gtk_model_menu_bind (GTK_MENU_SHELL (menubar), model, NULL, FALSE);
334   gtk_model_menu_populate (GTK_MENU_SHELL (menubar), accels);
335
336   return menubar;
337 }
338
339 /**
340  * gtk_menu_bar_new_from_model:
341  * @model: a #GMenuModel
342  *
343  * Creates a new #GtkMenuBar and populates it with menu items
344  * and submenus according to @model.
345  *
346  * The created menu items are connected to actions found in the
347  * #GtkApplicationWindow to which the menu bar belongs - typically
348  * by means of being contained within the #GtkApplicationWindows
349  * widget hierarchy.
350  *
351  * Returns: a new #GtkMenuBar
352  *
353  * Since: 3.4
354  */
355 GtkWidget *
356 gtk_menu_bar_new_from_model (GMenuModel *model)
357 {
358   GtkWidget *menubar;
359
360   menubar = gtk_menu_bar_new ();
361
362   gtk_model_menu_bind (GTK_MENU_SHELL (menubar), model, NULL, FALSE);
363   gtk_model_menu_populate (GTK_MENU_SHELL (menubar), NULL);
364
365   return menubar;
366 }