]> Pileus Git - ~andy/gtk/blob - gtk/gtkmodelmenu.c
modelmenu: listen for toplevel changes on the attach widget
[~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   GActionObservable *actions;
36   GMenuModel        *model;
37   GtkAccelGroup     *accels;
38   GtkMenuShell      *shell;
39   guint              update_idle;
40   GSList            *connected;
41   gboolean           with_separators;
42   gint               n_items;
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                                                  gboolean with_separators);
54
55 static void
56 gtk_model_menu_binding_free (gpointer data)
57 {
58   GtkModelMenuBinding *binding = data;
59
60   /* disconnect all existing signal handlers */
61   while (binding->connected)
62     {
63       g_signal_handlers_disconnect_by_func (binding->connected->data, gtk_model_menu_binding_items_changed, binding);
64       g_object_unref (binding->connected->data);
65
66       binding->connected = g_slist_delete_link (binding->connected, binding->connected);
67     }
68
69   if (binding->actions)
70     g_object_unref (binding->actions);
71   g_object_unref (binding->model);
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                                     gint                  item_index,
80                                     gchar               **heading)
81 {
82   GMenuModel *section;
83
84   if ((section = g_menu_model_get_item_link (model, item_index, "section")))
85     {
86       g_menu_model_get_item_attribute (model, item_index, "label", "s", heading);
87       gtk_model_menu_binding_append_model (binding, section, FALSE);
88     }
89   else
90     {
91       GtkMenuItem *item;
92
93       item = gtk_model_menu_item_new (model, item_index, binding->actions, binding->accels);
94       gtk_menu_shell_append (binding->shell, GTK_WIDGET (item));
95       gtk_widget_show (GTK_WIDGET (item));
96       binding->n_items++;
97     }
98 }
99
100 static void
101 gtk_model_menu_binding_append_model (GtkModelMenuBinding *binding,
102                                      GMenuModel          *model,
103                                      gboolean             with_separators)
104 {
105   gint n, i;
106
107   g_signal_connect (model, "items-changed", G_CALLBACK (gtk_model_menu_binding_items_changed), binding);
108   binding->connected = g_slist_prepend (binding->connected, g_object_ref (model));
109
110   /* Deciding if we should show a separator is a bit difficult.  There
111    * are two types of separators:
112    *
113    *  - section headings (when sections have 'label' property)
114    *
115    *  - normal separators automatically put between sections
116    *
117    * The easiest way to think about it is that a section usually has a
118    * separator (or heading) immediately before it.
119    *
120    * There are three exceptions to this general rule:
121    *
122    *  - empty sections don't get separators or headings
123    *
124    *  - sections only get separators and headings at the toplevel of a
125    *    menu (ie: no separators on nested sections or in menubars)
126    *
127    *  - the first section in the menu doesn't get a normal separator,
128    *    but it can get a header (if it's not empty)
129    *
130    * Unfortunately, we cannot simply check the size of the section in
131    * order to determine if we should place a header: the section may
132    * contain other sections that are themselves empty.  Instead, we need
133    * to append the section, and check if we ended up with any actual
134    * content.  If we did, then we need to insert before that content.
135    * We use 'our_position' to keep track of this.
136    */
137
138   n = g_menu_model_get_n_items (model);
139
140   for (i = 0; i < n; i++)
141     {
142       gint our_position = binding->n_items;
143       gchar *heading = NULL;
144
145       gtk_model_menu_binding_append_item (binding, model, i, &heading);
146
147       if (with_separators && our_position < binding->n_items)
148         {
149           GtkWidget *separator = NULL;
150
151           if (heading)
152             {
153               separator = gtk_menu_item_new_with_label (heading);
154               gtk_widget_set_sensitive (separator, FALSE);
155             }
156           else if (our_position > 0)
157             separator = gtk_separator_menu_item_new ();
158
159           if (separator)
160             {
161               gtk_menu_shell_insert (binding->shell, separator, our_position);
162               gtk_widget_show (separator);
163               binding->n_items++;
164             }
165         }
166
167       g_free (heading);
168     }
169 }
170
171 static void
172 gtk_model_menu_binding_populate (GtkModelMenuBinding *binding)
173 {
174   GList *children;
175
176   /* remove current children */
177   children = gtk_container_get_children (GTK_CONTAINER (binding->shell));
178   while (children)
179     {
180       gtk_container_remove (GTK_CONTAINER (binding->shell), children->data);
181       children = g_list_delete_link (children, children);
182     }
183
184   binding->n_items = 0;
185
186   /* add new items from the model */
187   gtk_model_menu_binding_append_model (binding, binding->model, binding->with_separators);
188 }
189
190 static gboolean
191 gtk_model_menu_binding_handle_changes (gpointer user_data)
192 {
193   GtkModelMenuBinding *binding = user_data;
194
195   /* disconnect all existing signal handlers */
196   while (binding->connected)
197     {
198       g_signal_handlers_disconnect_by_func (binding->connected->data, gtk_model_menu_binding_items_changed, binding);
199       g_object_unref (binding->connected->data);
200
201       binding->connected = g_slist_delete_link (binding->connected, binding->connected);
202     }
203
204   gtk_model_menu_binding_populate (binding);
205
206   binding->update_idle = 0;
207
208   g_object_unref (binding->shell);
209
210   return G_SOURCE_REMOVE;
211 }
212
213 static void
214 gtk_model_menu_binding_items_changed (GMenuModel *model,
215                                       gint        position,
216                                       gint        removed,
217                                       gint        added,
218                                       gpointer    user_data)
219 {
220   GtkModelMenuBinding *binding = user_data;
221
222   if (binding->update_idle == 0)
223     {
224       binding->update_idle = gdk_threads_add_idle (gtk_model_menu_binding_handle_changes, user_data);
225       g_object_ref (binding->shell);
226     }
227 }
228
229 static void
230 gtk_model_menu_bind (GtkMenuShell      *shell,
231                      GMenuModel        *model,
232                      gboolean           with_separators)
233 {
234   GtkModelMenuBinding *binding;
235
236   binding = g_slice_new (GtkModelMenuBinding);
237   binding->model = g_object_ref (model);
238   binding->actions = NULL;
239   binding->accels = NULL;
240   binding->shell = shell;
241   binding->update_idle = 0;
242   binding->connected = NULL;
243   binding->with_separators = with_separators;
244
245   g_object_set_data_full (G_OBJECT (shell), "gtk-model-menu-binding", binding, gtk_model_menu_binding_free);
246 }
247
248
249 static void
250 gtk_model_menu_populate (GtkMenuShell      *shell,
251                          GActionObservable *actions,
252                          GtkAccelGroup     *accels)
253 {
254   GtkModelMenuBinding *binding;
255
256   binding = (GtkModelMenuBinding*) g_object_get_data (G_OBJECT (shell), "gtk-model-menu-binding");
257
258   binding->actions = g_object_ref (actions);
259   binding->accels = accels;
260
261   gtk_model_menu_binding_populate (binding);
262 }
263
264 GtkWidget *
265 gtk_model_menu_create_menu (GMenuModel        *model,
266                             GActionObservable *actions,
267                             GtkAccelGroup     *accels)
268 {
269   GtkWidget *menu;
270
271   menu = gtk_menu_new ();
272   gtk_menu_set_accel_group (GTK_MENU (menu), accels);
273
274   gtk_model_menu_bind (GTK_MENU_SHELL (menu), model, TRUE);
275   gtk_model_menu_populate (GTK_MENU_SHELL (menu), actions, accels);
276
277   return menu;
278 }
279
280 static void
281 gtk_model_menu_connect_app_window (GtkMenu *menu,
282                                    GtkApplicationWindow *window)
283 {
284   GActionObservable *actions;
285   GtkAccelGroup *accels;
286
287   actions = gtk_application_window_get_observable (window);
288   accels = gtk_application_window_get_accel_group (window);
289
290   gtk_menu_set_accel_group (menu, accels);
291   gtk_model_menu_populate (GTK_MENU_SHELL (menu), actions, accels);
292 }
293
294 static void
295 attach_widget_hierarchy_changed (GtkWidget *attach_widget,
296                                  GtkWidget *previous_toplevel,
297                                  gpointer user_data)
298 {
299   GtkWidget *toplevel;
300   GtkMenu *menu = user_data;
301
302   toplevel = gtk_widget_get_toplevel (attach_widget);
303   if (GTK_IS_APPLICATION_WINDOW (toplevel))
304     gtk_model_menu_connect_app_window (menu, GTK_APPLICATION_WINDOW (toplevel));
305 }
306
307 static void
308 notify_attach (GtkMenu    *menu,
309                GParamSpec *pspec,
310                gpointer    data)
311 {
312   GtkWidget *attach_widget, *toplevel;
313
314   attach_widget = g_object_get_data (G_OBJECT (menu), MODEL_MENU_WIDGET_DATA);
315   if (attach_widget != NULL)
316     {
317       g_signal_handlers_disconnect_by_func (attach_widget, attach_widget_hierarchy_changed, menu);
318       g_object_set_data (G_OBJECT (menu), MODEL_MENU_WIDGET_DATA, NULL);
319     }
320
321   attach_widget = gtk_menu_get_attach_widget (menu);
322   if (!attach_widget)
323     return;
324
325   toplevel = gtk_widget_get_toplevel (attach_widget);
326   if (GTK_IS_APPLICATION_WINDOW (toplevel))
327     {
328       gtk_model_menu_connect_app_window (menu, GTK_APPLICATION_WINDOW (toplevel));
329     }
330   else
331     {
332       g_object_set_data (G_OBJECT (menu), MODEL_MENU_WIDGET_DATA, attach_widget);
333       g_signal_connect_object (attach_widget, "hierarchy-changed",
334                                G_CALLBACK (attach_widget_hierarchy_changed), menu, 0);
335     }
336 }
337
338 /**
339  * gtk_menu_new_from_model:
340  * @model: a #GMenuModel
341  *
342  * Creates a #GtkMenu and populates it with menu items and
343  * submenus according to @model.
344  *
345  * The created menu items are connected to actions found in the
346  * #GtkApplicationWindow to which the menu belongs - typically
347  * by means of being attached to a widget (see gtk_menu_attach_to_widget())
348  * that is contained within the #GtkApplicationWindows widget hierarchy.
349  *
350  * Returns: a new #GtkMenu
351  *
352  * Since: 3.4
353  */
354 GtkWidget *
355 gtk_menu_new_from_model (GMenuModel *model)
356 {
357   GtkWidget *menu;
358
359   menu = gtk_menu_new ();
360   gtk_model_menu_bind (GTK_MENU_SHELL (menu), model, TRUE);
361   g_signal_connect (menu, "notify::attach-widget",
362                     G_CALLBACK (notify_attach), NULL);
363
364   return menu;
365 }
366
367 GtkWidget *
368 gtk_model_menu_create_menu_bar (GMenuModel        *model,
369                                 GActionObservable *actions,
370                                 GtkAccelGroup     *accels)
371 {
372   GtkWidget *menubar;
373
374   menubar = gtk_menu_bar_new ();
375
376   gtk_model_menu_bind (GTK_MENU_SHELL (menubar), model, FALSE);
377   gtk_model_menu_populate (GTK_MENU_SHELL (menubar), actions, accels);
378
379   return menubar;
380 }
381
382 static void
383 hierarchy_changed (GtkMenuShell *shell,
384                    GObject      *previous_toplevel,
385                    gpointer      data)
386 {
387   GtkWidget *toplevel;
388   GActionObservable *actions;
389   GtkAccelGroup *accels;
390
391   toplevel = gtk_widget_get_toplevel (GTK_WIDGET (shell));
392   if (GTK_IS_APPLICATION_WINDOW (toplevel))
393     {
394       actions = gtk_application_window_get_observable (GTK_APPLICATION_WINDOW (toplevel));
395       accels = gtk_application_window_get_accel_group (GTK_APPLICATION_WINDOW (toplevel));
396
397       gtk_model_menu_populate (shell, actions, accels);
398     }
399 }
400
401 /**
402  * gtk_menu_bar_new_from_model:
403  * @model: a #GMenuModel
404  *
405  * Creates a new #GtkMenuBar and populates it with menu items
406  * and submenus according to @model.
407  *
408  * The created menu items are connected to actions found in the
409  * #GtkApplicationWindow to which the menu bar belongs - typically
410  * by means of being contained within the #GtkApplicationWindows
411  * widget hierarchy.
412  *
413  * Returns: a new #GtkMenuBar
414  *
415  * Since: 3.4
416  */
417 GtkWidget *
418 gtk_menu_bar_new_from_model (GMenuModel *model)
419 {
420   GtkWidget *menubar;
421
422   menubar = gtk_menu_bar_new ();
423
424   gtk_model_menu_bind (GTK_MENU_SHELL (menubar), model, FALSE);
425
426   g_signal_connect (menubar, "hierarchy-changed",
427                     G_CALLBACK (hierarchy_changed), NULL);
428
429   return menubar;
430 }