Doc updates to HildonAppMenu
[hildon] / hildon / hildon-app-menu.c
1 /*
2  * This file is a part of hildon
3  *
4  * Copyright (C) 2008 Nokia Corporation, all rights reserved.
5  *
6  * Contact: Rodrigo Novo <rodrigo.novo@nokia.com>
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU Lesser Public License as published by
10  * the Free Software Foundation; version 2 of the license.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Lesser Public License for more details.
16  *
17  */
18
19 /**
20  * SECTION:hildon-app-menu
21  * @short_description: Widget representing the application menu in the Hildon framework.
22  *
23  * #HildonAppMenu is a GTK widget which represents an application menu
24  * in the Hildon framework.
25  *
26  * This menu opens from the top of the screen and contains a number of
27  * entries (#GtkButton) organized in one or two columns, depending on
28  * the size of the screen (the number of columns changes automatically
29  * if the screen is resized). Entries are added left to right and top
30  * to bottom.
31  *
32  * Besides that, #HildonAppMenu can contain a group of filter buttons
33  * (#GtkToggleButton or #GtkRadioButton). Filters are meant to change
34  * the way data is presented in the application, rather than change
35  * the layout of the menu itself. For example, a file manager can have
36  * filters to decide the order used to display a list of files (name,
37  * date, size, etc.).
38  *
39  * To use a #HildonAppMenu, add it to a #HildonWindow using
40  * hildon_window_set_app_menu(). The menu will appear when the user
41  * presses the window title bar. Alternatively, you can show it by
42  * hand using hildon_app_menu_popup().
43  *
44  * The menu will be automatically hidden when one of its buttons is
45  * clicked. Use g_signal_connect_after() when connecting callbacks to
46  * buttons to make sure that they're called after the menu
47  * disappears. Alternatively, you can add the button to the menu
48  * before connecting any callback.
49  *
50  * Although implemented with a #GtkWindow, #HildonAppMenu behaves like
51  * a normal ref-counted widget, so g_object_ref(), g_object_unref(),
52  * g_object_ref_sink() and friends will behave just like with any
53  * other non-toplevel widget.
54  *
55  * <example>
56  * <title>Creating a HildonAppMenu</title>
57  * <programlisting>
58  * GtkWidget *win;
59  * HildonAppMenu *menu;
60  * GtkWidget *button;
61  * GtkWidget *filter;
62  * <!-- -->
63  * win = hildon_stackable_window_new ();
64  * menu = HILDON_APP_MENU (hildon_app_menu_new ());
65  * <!-- -->
66  * // Create a button and add it to the menu
67  * button = gtk_button_new_with_label ("Menu command one");
68  * g_signal_connect_after (button, "clicked", G_CALLBACK (button_one_clicked), userdata);
69  * hildon_app_menu_append (menu, GTK_BUTTON (button));
70  * <!-- -->
71  * // Another button
72  * button = gtk_button_new_with_label ("Menu command two");
73  * g_signal_connect_after (button, "clicked", G_CALLBACK (button_two_clicked), userdata);
74  * hildon_app_menu_append (menu, GTK_BUTTON (button));
75  * <!-- -->
76  * // Create a filter and add it to the menu
77  * filter = gtk_radio_button_new_with_label (NULL, "Filter one");
78  * gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (filter), FALSE);
79  * g_signal_connect_after (filter, "clicked", G_CALLBACK (filter_one_clicked), userdata);
80  * hildon_app_menu_add_filter (menu, GTK_BUTTON (filter));
81  * <!-- -->
82  * // Add a new filter
83  * filter = gtk_radio_button_new_with_label_from_widget (GTK_RADIO_BUTTON (filter), "Filter two");
84  * gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (filter), FALSE);
85  * g_signal_connect_after (filter, "clicked", G_CALLBACK (filter_two_clicked), userdata);
86  * hildon_app_menu_add_filter (menu, GTK_BUTTON (filter));
87  * <!-- -->
88  * // Show all menu items
89  * gtk_widget_show_all (GTK_WIDGET (menu));
90  * <!-- -->
91  * // Add the menu to the window
92  * hildon_window_set_app_menu (HILDON_WINDOW (win), menu);
93  * </programlisting>
94  * </example>
95  *
96  */
97
98 #include                                        <string.h>
99 #include                                        <X11/Xatom.h>
100 #include                                        <gdk/gdkx.h>
101
102 #include                                        "hildon-gtk.h"
103 #include                                        "hildon-app-menu.h"
104 #include                                        "hildon-app-menu-private.h"
105 #include                                        "hildon-window.h"
106 #include                                        "hildon-banner.h"
107
108 static GdkWindow *
109 grab_transfer_window_get                        (GtkWidget *widget);
110
111 static void
112 hildon_app_menu_repack_items                    (HildonAppMenu *menu,
113                                                  gint           start_from);
114
115 static void
116 hildon_app_menu_repack_filters                  (HildonAppMenu *menu);
117
118 static gboolean
119 can_activate_accel                              (GtkWidget *widget,
120                                                  guint      signal_id,
121                                                  gpointer   user_data);
122
123 static void
124 item_visibility_changed                         (GtkWidget     *item,
125                                                  GParamSpec    *arg1,
126                                                  HildonAppMenu *menu);
127
128 static void
129 filter_visibility_changed                       (GtkWidget     *item,
130                                                  GParamSpec    *arg1,
131                                                  HildonAppMenu *menu);
132
133 static void
134 remove_item_from_list                           (GList    **list,
135                                                  gpointer   item);
136
137 static void
138 hildon_app_menu_apply_style                     (GtkWidget *widget);
139
140 G_DEFINE_TYPE (HildonAppMenu, hildon_app_menu, GTK_TYPE_WINDOW);
141
142 /**
143  * hildon_app_menu_new:
144  *
145  * Creates a new #HildonAppMenu.
146  *
147  * Return value: A #HildonAppMenu.
148  *
149  * Since: 2.2
150  **/
151 GtkWidget *
152 hildon_app_menu_new                             (void)
153 {
154     GtkWidget *menu = g_object_new (HILDON_TYPE_APP_MENU, NULL);
155     return menu;
156 }
157
158 /**
159  * hildon_app_menu_insert:
160  * @menu : A #HildonAppMenu
161  * @item : A #GtkButton to add to the #HildonAppMenu
162  * @position : The position in the item list where @item is added (from 0 to n-1).
163  *
164  * Adds @item to @menu at the position indicated by @position.
165  *
166  * Since: 2.2
167  */
168 void
169 hildon_app_menu_insert                          (HildonAppMenu *menu,
170                                                  GtkButton     *item,
171                                                  gint           position)
172 {
173     HildonAppMenuPrivate *priv;
174
175     g_return_if_fail (HILDON_IS_APP_MENU (menu));
176     g_return_if_fail (GTK_IS_BUTTON (item));
177
178     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
179
180     /* Force widget size */
181     hildon_gtk_widget_set_theme_size (GTK_WIDGET (item),
182                                       HILDON_SIZE_FINGER_HEIGHT | HILDON_SIZE_AUTO_WIDTH);
183
184     /* Add the item to the menu */
185     g_object_ref_sink (item);
186     priv->buttons = g_list_insert (priv->buttons, item, position);
187     if (GTK_WIDGET_VISIBLE (item))
188         hildon_app_menu_repack_items (menu, position);
189
190     /* Enable accelerators */
191     g_signal_connect (item, "can-activate-accel", G_CALLBACK (can_activate_accel), NULL);
192
193     /* Close the menu when the button is clicked */
194     g_signal_connect_swapped (item, "clicked", G_CALLBACK (gtk_widget_hide), menu);
195     g_signal_connect (item, "notify::visible", G_CALLBACK (item_visibility_changed), menu);
196
197     /* Remove item from list when it is destroyed */
198     g_object_weak_ref (G_OBJECT (item), (GWeakNotify) remove_item_from_list, &(priv->buttons));
199 }
200
201 /**
202  * hildon_app_menu_append:
203  * @menu : A #HildonAppMenu
204  * @item : A #GtkButton to add to the #HildonAppMenu
205  *
206  * Adds @item to the end of the menu's item list.
207  *
208  * Since: 2.2
209  */
210 void
211 hildon_app_menu_append                          (HildonAppMenu *menu,
212                                                  GtkButton     *item)
213 {
214     hildon_app_menu_insert (menu, item, -1);
215 }
216
217 /**
218  * hildon_app_menu_prepend:
219  * @menu : A #HildonAppMenu
220  * @item : A #GtkButton to add to the #HildonAppMenu
221  *
222  * Adds @item to the beginning of the menu's item list.
223  *
224  * Since: 2.2
225  */
226 void
227 hildon_app_menu_prepend                         (HildonAppMenu *menu,
228                                                  GtkButton     *item)
229 {
230     hildon_app_menu_insert (menu, item, 0);
231 }
232
233 /**
234  * hildon_app_menu_reorder_child:
235  * @menu : A #HildonAppMenu
236  * @item : A #GtkButton to move
237  * @position : The new position to place @item (from 0 to n-1).
238  *
239  * Moves a #GtkButton to a new position within #HildonAppMenu.
240  *
241  * Since: 2.2
242  */
243 void
244 hildon_app_menu_reorder_child                   (HildonAppMenu *menu,
245                                                  GtkButton     *item,
246                                                  gint           position)
247 {
248     HildonAppMenuPrivate *priv;
249     gint old_position;
250
251     g_return_if_fail (HILDON_IS_APP_MENU (menu));
252     g_return_if_fail (GTK_IS_BUTTON (item));
253     g_return_if_fail (position >= 0);
254
255     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
256     old_position = g_list_index (priv->buttons, item);
257
258     g_return_if_fail (old_position >= 0);
259
260     /* Move the item */
261     priv->buttons = g_list_remove (priv->buttons, item);
262     priv->buttons = g_list_insert (priv->buttons, item, position);
263
264     hildon_app_menu_repack_items (menu, MIN (old_position, position));
265 }
266
267 /**
268  * hildon_app_menu_add_filter:
269  * @menu : A #HildonAppMenu
270  * @filter : A #GtkButton to add to the #HildonAppMenu.
271  *
272  * Adds the @filter to @menu.
273  *
274  * Since: 2.2
275  */
276 void
277 hildon_app_menu_add_filter                      (HildonAppMenu *menu,
278                                                  GtkButton *filter)
279 {
280     HildonAppMenuPrivate *priv;
281
282     g_return_if_fail (HILDON_IS_APP_MENU (menu));
283     g_return_if_fail (GTK_IS_BUTTON (filter));
284
285     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
286
287     /* Force widget size */
288     hildon_gtk_widget_set_theme_size (GTK_WIDGET (filter),
289                                       HILDON_SIZE_FINGER_HEIGHT | HILDON_SIZE_AUTO_WIDTH);
290
291     /* Add the filter to the menu */
292     g_object_ref_sink (filter);
293     priv->filters = g_list_append (priv->filters, filter);
294     if (GTK_WIDGET_VISIBLE (filter))
295         hildon_app_menu_repack_filters (menu);
296
297     /* Enable accelerators */
298     g_signal_connect (filter, "can-activate-accel", G_CALLBACK (can_activate_accel), NULL);
299
300     /* Close the menu when the button is clicked */
301     g_signal_connect_swapped (filter, "clicked", G_CALLBACK (gtk_widget_hide), menu);
302     g_signal_connect (filter, "notify::visible", G_CALLBACK (filter_visibility_changed), menu);
303
304     /* Remove filter from list when it is destroyed */
305     g_object_weak_ref (G_OBJECT (filter), (GWeakNotify) remove_item_from_list, &(priv->filters));
306 }
307
308 static void
309 hildon_app_menu_set_columns                     (HildonAppMenu *menu,
310                                                  guint          columns)
311 {
312     HildonAppMenuPrivate *priv;
313
314     g_return_if_fail (HILDON_IS_APP_MENU (menu));
315     g_return_if_fail (columns > 0);
316
317     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
318
319     if (columns != priv->columns) {
320         priv->columns = columns;
321         hildon_app_menu_repack_items (menu, 0);
322     }
323 }
324
325 static void
326 parent_window_topmost_notify                   (HildonWindow *parent_win,
327                                                 GParamSpec   *arg1,
328                                                 GtkWidget    *menu)
329 {
330     if (!hildon_window_get_is_topmost (parent_win))
331         gtk_widget_hide (menu);
332 }
333
334 static void
335 parent_window_unmapped                         (HildonWindow *parent_win,
336                                                 GtkWidget    *menu)
337 {
338     gtk_widget_hide (menu);
339 }
340
341 void G_GNUC_INTERNAL
342 hildon_app_menu_set_parent_window              (HildonAppMenu *self,
343                                                 GtkWindow     *parent_window)
344 {
345     HildonAppMenuPrivate *priv;
346
347     g_return_if_fail (HILDON_IS_APP_MENU (self));
348     g_return_if_fail (parent_window == NULL || GTK_IS_WINDOW (parent_window));
349
350     priv = HILDON_APP_MENU_GET_PRIVATE(self);
351
352     /* Disconnect old handlers, if any */
353     if (priv->parent_window) {
354         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_topmost_notify, self);
355         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_unmapped, self);
356     }
357
358     /* Connect a new handler */
359     if (parent_window) {
360         g_signal_connect (parent_window, "notify::is-topmost", G_CALLBACK (parent_window_topmost_notify), self);
361         g_signal_connect (parent_window, "unmap", G_CALLBACK (parent_window_unmapped), self);
362     }
363
364     priv->parent_window = parent_window;
365
366     if (parent_window == NULL && GTK_WIDGET_VISIBLE (self))
367         gtk_widget_hide (GTK_WIDGET (self));
368 }
369
370 gpointer G_GNUC_INTERNAL
371 hildon_app_menu_get_parent_window              (HildonAppMenu *self)
372 {
373     HildonAppMenuPrivate *priv;
374
375     g_return_val_if_fail (HILDON_IS_APP_MENU (self), NULL);
376
377     priv = HILDON_APP_MENU_GET_PRIVATE (self);
378
379     return priv->parent_window;
380 }
381
382 static void
383 screen_size_changed                            (GdkScreen     *screen,
384                                                 HildonAppMenu *menu)
385 {
386     hildon_app_menu_apply_style (GTK_WIDGET (menu));
387
388     if (gdk_screen_get_width (screen) > gdk_screen_get_height (screen)) {
389         hildon_app_menu_set_columns (menu, 2);
390     } else {
391         hildon_app_menu_set_columns (menu, 1);
392     }
393 }
394
395 static gboolean
396 can_activate_accel                              (GtkWidget *widget,
397                                                  guint      signal_id,
398                                                  gpointer   user_data)
399 {
400     return GTK_WIDGET_VISIBLE (widget);
401 }
402
403 static void
404 item_visibility_changed                         (GtkWidget     *item,
405                                                  GParamSpec    *arg1,
406                                                  HildonAppMenu *menu)
407 {
408     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (menu);
409
410     if (! priv->inhibit_repack)
411         hildon_app_menu_repack_items (menu, g_list_index (priv->buttons, item));
412 }
413
414 static void
415 filter_visibility_changed                       (GtkWidget     *item,
416                                                  GParamSpec    *arg1,
417                                                  HildonAppMenu *menu)
418 {
419     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (menu);
420
421     if (! priv->inhibit_repack)
422         hildon_app_menu_repack_filters (menu);
423 }
424
425 static void
426 remove_item_from_list                           (GList    **list,
427                                                  gpointer   item)
428 {
429     *list = g_list_remove (*list, item);
430 }
431
432 static void
433 hildon_app_menu_show_all                        (GtkWidget *widget)
434 {
435     HildonAppMenu *menu = HILDON_APP_MENU (widget);
436     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
437
438     priv->inhibit_repack = TRUE;
439
440     /* Show children, but not self. */
441     g_list_foreach (priv->buttons, (GFunc) gtk_widget_show_all, NULL);
442     g_list_foreach (priv->filters, (GFunc) gtk_widget_show_all, NULL);
443
444     priv->inhibit_repack = FALSE;
445
446     hildon_app_menu_repack_items (menu, 0);
447     hildon_app_menu_repack_filters (menu);
448 }
449
450
451 static void
452 hildon_app_menu_hide_all                        (GtkWidget *widget)
453 {
454     HildonAppMenu *menu = HILDON_APP_MENU (widget);
455     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
456
457     priv->inhibit_repack = TRUE;
458
459     /* Hide children, but not self. */
460     g_list_foreach (priv->buttons, (GFunc) gtk_widget_hide_all, NULL);
461     g_list_foreach (priv->filters, (GFunc) gtk_widget_hide_all, NULL);
462
463     priv->inhibit_repack = FALSE;
464
465     hildon_app_menu_repack_items (menu, 0);
466     hildon_app_menu_repack_filters (menu);
467 }
468
469 /*
470  * There's a race condition that can freeze the UI if a dialog appears
471  * between a HildonAppMenu and its parent window, see NB#100468
472  */
473 static gboolean
474 hildon_app_menu_find_intruder                   (gpointer data)
475 {
476     GtkWidget *widget = GTK_WIDGET (data);
477     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
478
479     priv->find_intruder_idle_id = 0;
480
481     /* If there's a window between the menu and its parent window, hide the menu */
482     if (priv->parent_window) {
483         gboolean intruder_found = FALSE;
484         GdkScreen *screen = gtk_widget_get_screen (widget);
485         GList *stack = gdk_screen_get_window_stack (screen);
486         GList *parent_pos = g_list_find (stack, GTK_WIDGET (priv->parent_window)->window);
487         GList *toplevels = gtk_window_list_toplevels ();
488         GList *i;
489
490         for (i = toplevels; i != NULL && !intruder_found; i = i->next) {
491             if (i->data != widget && i->data != priv->parent_window) {
492                 if (g_list_find (parent_pos, GTK_WIDGET (i->data)->window)) {
493                     /* HildonBanners are not closed automatically when
494                      * a new window appears, so we must close them by
495                      * hand to make the AppMenu work as expected.
496                      * Yes, this is a hack. See NB#111027 */
497                     if (HILDON_IS_BANNER (i->data)) {
498                         gtk_widget_hide (i->data);
499                     } else {
500                         intruder_found = TRUE;
501                     }
502                 }
503             }
504         }
505
506         g_list_foreach (stack, (GFunc) g_object_unref, NULL);
507         g_list_free (stack);
508         g_list_free (toplevels);
509
510         if (intruder_found)
511             gtk_widget_hide (widget);
512     }
513
514     return FALSE;
515 }
516
517 static void
518 hildon_app_menu_map                             (GtkWidget *widget)
519 {
520     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
521
522     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->map (widget);
523
524     /* Grab pointer and keyboard */
525     if (priv->transfer_window == NULL) {
526         gboolean has_grab = FALSE;
527
528         priv->transfer_window = grab_transfer_window_get (widget);
529
530         if (gdk_pointer_grab (priv->transfer_window, TRUE,
531                               GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
532                               GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
533                               GDK_POINTER_MOTION_MASK, NULL, NULL,
534                               GDK_CURRENT_TIME) == GDK_GRAB_SUCCESS) {
535             if (gdk_keyboard_grab (priv->transfer_window, TRUE,
536                                    GDK_CURRENT_TIME) == GDK_GRAB_SUCCESS) {
537                 has_grab = TRUE;
538             } else {
539                 gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
540                                             GDK_CURRENT_TIME);
541             }
542         }
543
544         if (has_grab) {
545             gtk_grab_add (widget);
546         } else {
547             gdk_window_destroy (priv->transfer_window);
548             priv->transfer_window = NULL;
549         }
550     }
551
552     /* Make the menu temporary when it's mapped, so it's closed if a
553      * new window appears */
554     gtk_window_set_is_temporary (GTK_WINDOW (widget), TRUE);
555
556     priv->find_intruder_idle_id = gdk_threads_add_idle (hildon_app_menu_find_intruder, widget);
557 }
558
559 static void
560 hildon_app_menu_unmap                           (GtkWidget *widget)
561 {
562     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
563
564     /* Remove the grab */
565     if (priv->transfer_window != NULL) {
566         gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
567                                     GDK_CURRENT_TIME);
568         gtk_grab_remove (widget);
569
570         gdk_window_destroy (priv->transfer_window);
571         priv->transfer_window = NULL;
572     }
573
574     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unmap (widget);
575
576     gtk_window_set_is_temporary (GTK_WINDOW (widget), FALSE);
577 }
578
579 static void
580 hildon_app_menu_grab_notify                     (GtkWidget *widget,
581                                                  gboolean   was_grabbed)
582 {
583     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->grab_notify)
584         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->grab_notify (widget, was_grabbed);
585
586     if (!was_grabbed && GTK_WIDGET_VISIBLE (widget))
587         gtk_widget_hide (widget);
588 }
589
590 static gboolean
591 hildon_app_menu_hide_idle                       (gpointer widget)
592 {
593     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
594     gtk_widget_hide (GTK_WIDGET (widget));
595     priv->hide_idle_id = 0;
596     return FALSE;
597 }
598
599 /* Send keyboard accelerators to the parent window, if necessary.
600  * This code is heavily based on gtk_menu_key_press ()
601  */
602 static gboolean
603 hildon_app_menu_key_press                       (GtkWidget   *widget,
604                                                  GdkEventKey *event)
605 {
606     GtkWindow *parent_window;
607     HildonAppMenuPrivate *priv;
608
609     g_return_val_if_fail (HILDON_IS_APP_MENU (widget), FALSE);
610     g_return_val_if_fail (event != NULL, FALSE);
611
612     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->key_press_event (widget, event))
613         return TRUE;
614
615     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
616     parent_window = priv->parent_window;
617
618     if (parent_window) {
619         guint accel_key, accel_mods;
620         GdkModifierType consumed_modifiers;
621         GdkDisplay *display;
622         GSList *accel_groups;
623         GSList *list;
624
625         display = gtk_widget_get_display (widget);
626
627         /* Figure out what modifiers went into determining the key symbol */
628         gdk_keymap_translate_keyboard_state (gdk_keymap_get_for_display (display),
629                                              event->hardware_keycode, event->state, event->group,
630                                              NULL, NULL, NULL, &consumed_modifiers);
631
632         accel_key = gdk_keyval_to_lower (event->keyval);
633         accel_mods = event->state & gtk_accelerator_get_default_mod_mask () & ~consumed_modifiers;
634
635         /* If lowercasing affects the keysym, then we need to include SHIFT in the modifiers,
636          * We re-upper case when we match against the keyval, but display and save in caseless form.
637          */
638         if (accel_key != event->keyval)
639             accel_mods |= GDK_SHIFT_MASK;
640
641         accel_groups = gtk_accel_groups_from_object (G_OBJECT (parent_window));
642
643         for (list = accel_groups; list; list = list->next) {
644             GtkAccelGroup *accel_group = list->data;
645
646             if (gtk_accel_group_query (accel_group, accel_key, accel_mods, NULL)) {
647                 gtk_window_activate_key (parent_window, event);
648                 priv->hide_idle_id = gdk_threads_add_idle (hildon_app_menu_hide_idle, widget);
649                 break;
650             }
651         }
652     }
653
654     return TRUE;
655 }
656
657 static gboolean
658 hildon_app_menu_button_press                    (GtkWidget *widget,
659                                                  GdkEventButton *event)
660 {
661     int x, y;
662     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
663
664     gdk_window_get_position (widget->window, &x, &y);
665
666     /* Whether the button has been pressed outside the widget */
667     priv->pressed_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
668                              event->y_root < y || event->y_root > y + widget->allocation.height);
669
670     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event) {
671         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event (widget, event);
672     } else {
673         return FALSE;
674     }
675 }
676
677 static gboolean
678 hildon_app_menu_button_release                  (GtkWidget *widget,
679                                                  GdkEventButton *event)
680 {
681     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
682
683     if (priv->pressed_outside) {
684         int x, y;
685         gboolean released_outside;
686
687         gdk_window_get_position (widget->window, &x, &y);
688
689         /* Whether the button has been released outside the widget */
690         released_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
691                             event->y_root < y || event->y_root > y + widget->allocation.height);
692
693         if (released_outside) {
694             gtk_widget_hide (widget);
695         }
696
697         priv->pressed_outside = FALSE; /* Always reset pressed_outside to FALSE */
698     }
699
700     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event) {
701         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event (widget, event);
702     } else {
703         return FALSE;
704     }
705 }
706
707 static gboolean
708 hildon_app_menu_delete_event_handler            (GtkWidget   *widget,
709                                                  GdkEventAny *event)
710 {
711     /* Hide the menu if it receives a delete-event, but don't destroy it */
712     gtk_widget_hide (widget);
713     return TRUE;
714 }
715
716 /* Grab transfer window (based on the one from GtkMenu) */
717 static GdkWindow *
718 grab_transfer_window_get                        (GtkWidget *widget)
719 {
720     GdkWindow *window;
721     GdkWindowAttr attributes;
722     gint attributes_mask;
723
724     attributes.x = 0;
725     attributes.y = 0;
726     attributes.width = 10;
727     attributes.height = 10;
728     attributes.window_type = GDK_WINDOW_TEMP;
729     attributes.wclass = GDK_INPUT_ONLY;
730     attributes.override_redirect = TRUE;
731     attributes.event_mask = 0;
732
733     attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_NOREDIR;
734
735     window = gdk_window_new (gtk_widget_get_root_window (widget),
736                                  &attributes, attributes_mask);
737     gdk_window_set_user_data (window, widget);
738
739     gdk_window_show (window);
740
741     return window;
742 }
743
744 static void
745 hildon_app_menu_size_request                    (GtkWidget      *widget,
746                                                  GtkRequisition *requisition)
747 {
748     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
749
750     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->size_request (widget, requisition);
751
752     requisition->width = priv->width_request;
753 }
754
755 static void
756 hildon_app_menu_realize                         (GtkWidget *widget)
757 {
758     Atom property, window_type;
759     Display *xdisplay;
760     GdkDisplay *gdkdisplay;
761     GdkScreen *screen;
762
763     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->realize (widget);
764
765     gdk_window_set_decorations (widget->window, GDK_DECOR_BORDER);
766
767     gdkdisplay = gdk_drawable_get_display (widget->window);
768     xdisplay = GDK_WINDOW_XDISPLAY (widget->window);
769
770     property = gdk_x11_get_xatom_by_name_for_display (gdkdisplay, "_NET_WM_WINDOW_TYPE");
771     window_type = XInternAtom (xdisplay, "_HILDON_WM_WINDOW_TYPE_APP_MENU", False);
772     XChangeProperty (xdisplay, GDK_WINDOW_XID (widget->window), property,
773                      XA_ATOM, 32, PropModeReplace, (guchar *) &window_type, 1);
774
775     /* Detect any screen changes */
776     screen = gtk_widget_get_screen (widget);
777     g_signal_connect (screen, "size-changed", G_CALLBACK (screen_size_changed), widget);
778
779     /* Force menu to set the initial layout */
780     screen_size_changed (screen, HILDON_APP_MENU (widget));
781 }
782
783 static void
784 hildon_app_menu_unrealize                       (GtkWidget *widget)
785 {
786     GdkScreen *screen = gtk_widget_get_screen (widget);
787     /* Disconnect "size-changed" signal handler */
788     g_signal_handlers_disconnect_by_func (screen, G_CALLBACK (screen_size_changed), widget);
789
790     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unrealize (widget);
791 }
792
793 static void
794 hildon_app_menu_apply_style                     (GtkWidget *widget)
795 {
796     GdkScreen *screen;
797     guint horizontal_spacing, vertical_spacing, filter_vertical_spacing;
798     guint inner_border, external_border;
799     HildonAppMenuPrivate *priv;
800
801     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
802
803     gtk_widget_style_get (widget,
804                           "horizontal-spacing", &horizontal_spacing,
805                           "vertical-spacing", &vertical_spacing,
806                           "filter-vertical-spacing", &filter_vertical_spacing,
807                           "inner-border", &inner_border,
808                           "external-border", &external_border,
809                           NULL);
810
811     /* Set spacings */
812     gtk_table_set_row_spacings (priv->table, vertical_spacing);
813     gtk_table_set_col_spacings (priv->table, horizontal_spacing);
814     gtk_box_set_spacing (priv->vbox, filter_vertical_spacing);
815
816     /* Set inner border */
817     gtk_container_set_border_width (GTK_CONTAINER (widget), inner_border);
818
819     /* Compute width request */
820     screen = gtk_widget_get_screen (widget);
821     if (gdk_screen_get_width (screen) < gdk_screen_get_height (screen)) {
822         external_border = 0;
823     }
824     priv->width_request = gdk_screen_get_width (screen) - external_border * 2;
825     gtk_window_move (GTK_WINDOW (widget), external_border, 0);
826     gtk_widget_queue_resize (widget);
827 }
828
829 static void
830 hildon_app_menu_style_set                       (GtkWidget *widget,
831                                                  GtkStyle  *previous_style)
832 {
833     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set)
834         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set (widget, previous_style);
835
836     hildon_app_menu_apply_style (widget);
837 }
838
839 static void
840 hildon_app_menu_repack_filters                  (HildonAppMenu *menu)
841 {
842     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
843     GList *iter;
844
845     for (iter = priv->filters; iter != NULL; iter = iter->next) {
846         GtkWidget *filter = GTK_WIDGET (iter->data);
847         GtkWidget *parent = gtk_widget_get_parent (filter);
848         if (parent) {
849             g_object_ref (filter);
850             gtk_container_remove (GTK_CONTAINER (parent), filter);
851         }
852     }
853
854     for (iter = priv->filters; iter != NULL; iter = iter->next) {
855         GtkWidget *filter = GTK_WIDGET (iter->data);
856         if (GTK_WIDGET_VISIBLE (filter)) {
857             gtk_box_pack_start (GTK_BOX (priv->filters_hbox), filter, TRUE, TRUE, 0);
858             g_object_unref (filter);
859             /* GtkButton must be realized for accelerators to work */
860             gtk_widget_realize (filter);
861         }
862     }
863 }
864
865 /*
866  * When items displayed in the menu change (e.g, a new item is added,
867  * an item is hidden or the list is reordered), the layout must be
868  * updated. To do this we repack all items starting from a given one.
869  */
870 static void
871 hildon_app_menu_repack_items                    (HildonAppMenu *menu,
872                                                  gint           start_from)
873 {
874     HildonAppMenuPrivate *priv;
875     gint row, col, nvisible, i;
876     GList *iter;
877
878     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
879
880     i = nvisible = 0;
881     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
882         /* Count number of visible items */
883         if (GTK_WIDGET_VISIBLE (iter->data))
884             nvisible++;
885         /* Remove buttons from their parent */
886         if (start_from != -1 && i >= start_from) {
887             GtkWidget *item = GTK_WIDGET (iter->data);
888             GtkWidget *parent = gtk_widget_get_parent (item);
889             if (parent) {
890                 g_object_ref (item);
891                 gtk_container_remove (GTK_CONTAINER (parent), item);
892             }
893         }
894         i++;
895     }
896
897     /* If items have been removed, recalculate the size of the menu */
898     if (start_from != -1)
899         gtk_window_resize (GTK_WINDOW (menu), 1, 1);
900
901     /* Set the final size now to avoid unnecessary resizes later */
902     if (nvisible > 0)
903         gtk_table_resize (priv->table, ((nvisible - 1) / priv->columns) + 1, priv->columns);
904
905     /* Add buttons */
906     row = col = 0;
907     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
908         GtkWidget *item = GTK_WIDGET (iter->data);
909         if (GTK_WIDGET_VISIBLE (item)) {
910             /* Don't add an item to the table if it's already there */
911             if (gtk_widget_get_parent (item) == NULL) {
912                 gtk_table_attach_defaults (priv->table, item, col, col + 1, row, row + 1);
913                 g_object_unref (item);
914                 /* GtkButton must be realized for accelerators to work */
915                 gtk_widget_realize (item);
916             }
917             if (++col == priv->columns) {
918                 col = 0;
919                 row++;
920             }
921         }
922     }
923 }
924
925 /**
926  * hildon_app_menu_popup:
927  * @menu: a #HildonAppMenu
928  * @parent_window: a #GtkWindow
929  *
930  * Displays a menu on top of a window and makes it available for
931  * selection.
932  *
933  * Since: 2.2
934  **/
935 void
936 hildon_app_menu_popup                           (HildonAppMenu *menu,
937                                                  GtkWindow     *parent_window)
938 {
939     HildonAppMenuPrivate *priv;
940     gboolean show_menu = FALSE;
941     GList *i;
942
943     g_return_if_fail (HILDON_IS_APP_MENU (menu));
944     g_return_if_fail (GTK_IS_WINDOW (parent_window));
945
946     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
947
948     /* Don't show menu if it doesn't contain visible items */
949     for (i = priv->buttons; i && !show_menu; i = i->next)
950         show_menu = GTK_WIDGET_VISIBLE (i->data);
951
952     for (i = priv->filters; i && !show_menu; i = i->next)
953         show_menu = GTK_WIDGET_VISIBLE (i->data);
954
955     if (show_menu) {
956         hildon_app_menu_set_parent_window (menu, parent_window);
957         gtk_widget_show (GTK_WIDGET (menu));
958     }
959
960 }
961
962 /**
963  * hildon_app_menu_get_items:
964  * @menu: a #HildonAppMenu
965  *
966  * Returns a list of all items (regular items, not filters) contained
967  * in @menu.
968  *
969  * Returns: a newly-allocated list containing the items in @menu
970  *
971  * Since: 2.2
972  **/
973 GList *
974 hildon_app_menu_get_items                       (HildonAppMenu *menu)
975 {
976     HildonAppMenuPrivate *priv;
977
978     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
979
980     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
981
982     return g_list_copy (priv->buttons);
983 }
984
985 /**
986  * hildon_app_menu_get_filters:
987  * @menu: a #HildonAppMenu
988  *
989  * Returns a list of all filters contained in @menu.
990  *
991  * Returns: a newly-allocated list containing the filters in @menu
992  *
993  * Since: 2.2
994  **/
995 GList *
996 hildon_app_menu_get_filters                     (HildonAppMenu *menu)
997 {
998     HildonAppMenuPrivate *priv;
999
1000     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
1001
1002     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
1003
1004     return g_list_copy (priv->filters);
1005 }
1006
1007 static void
1008 hildon_app_menu_init                            (HildonAppMenu *menu)
1009 {
1010     GtkWidget *alignment;
1011     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
1012
1013     /* Initialize private variables */
1014     priv->parent_window = NULL;
1015     priv->transfer_window = NULL;
1016     priv->pressed_outside = FALSE;
1017     priv->inhibit_repack = FALSE;
1018     priv->buttons = NULL;
1019     priv->filters = NULL;
1020     priv->columns = 2;
1021     priv->width_request = -1;
1022     priv->find_intruder_idle_id = 0;
1023     priv->hide_idle_id = 0;
1024
1025     /* Create boxes and tables */
1026     priv->filters_hbox = GTK_BOX (gtk_hbox_new (TRUE, 0));
1027     priv->vbox = GTK_BOX (gtk_vbox_new (FALSE, 0));
1028     priv->table = GTK_TABLE (gtk_table_new (1, priv->columns, TRUE));
1029
1030     /* Align the filters to the center */
1031     alignment = gtk_alignment_new (0.5, 0.5, 0, 0);
1032     gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (priv->filters_hbox));
1033
1034     /* Pack everything */
1035     gtk_container_add (GTK_CONTAINER (menu), GTK_WIDGET (priv->vbox));
1036     gtk_box_pack_start (priv->vbox, alignment, TRUE, TRUE, 0);
1037     gtk_box_pack_start (priv->vbox, GTK_WIDGET (priv->table), TRUE, TRUE, 0);
1038
1039     /* This should be treated like a normal, ref-counted widget */
1040     g_object_force_floating (G_OBJECT (menu));
1041     GTK_WINDOW (menu)->has_user_ref_count = FALSE;
1042
1043     gtk_widget_show_all (GTK_WIDGET (priv->vbox));
1044 }
1045
1046 static void
1047 hildon_app_menu_finalize                        (GObject *object)
1048 {
1049     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(object);
1050
1051     if (priv->find_intruder_idle_id) {
1052         g_source_remove (priv->find_intruder_idle_id);
1053         priv->find_intruder_idle_id = 0;
1054     }
1055
1056     if (priv->hide_idle_id) {
1057         g_source_remove (priv->hide_idle_id);
1058         priv->hide_idle_id = 0;
1059     }
1060
1061     if (priv->parent_window) {
1062         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_topmost_notify, object);
1063         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_unmapped, object);
1064     }
1065
1066     if (priv->transfer_window)
1067         gdk_window_destroy (priv->transfer_window);
1068
1069     g_list_foreach (priv->buttons, (GFunc) g_object_unref, NULL);
1070     g_list_foreach (priv->filters, (GFunc) g_object_unref, NULL);
1071
1072     g_list_free (priv->buttons);
1073     g_list_free (priv->filters);
1074
1075     g_signal_handlers_destroy (object);
1076     G_OBJECT_CLASS (hildon_app_menu_parent_class)->finalize (object);
1077 }
1078
1079 static void
1080 hildon_app_menu_class_init                      (HildonAppMenuClass *klass)
1081 {
1082     GObjectClass *gobject_class = (GObjectClass *)klass;
1083     GtkWidgetClass *widget_class = (GtkWidgetClass *)klass;
1084
1085     gobject_class->finalize = hildon_app_menu_finalize;
1086     widget_class->show_all = hildon_app_menu_show_all;
1087     widget_class->hide_all = hildon_app_menu_hide_all;
1088     widget_class->map = hildon_app_menu_map;
1089     widget_class->unmap = hildon_app_menu_unmap;
1090     widget_class->realize = hildon_app_menu_realize;
1091     widget_class->unrealize = hildon_app_menu_unrealize;
1092     widget_class->grab_notify = hildon_app_menu_grab_notify;
1093     widget_class->key_press_event = hildon_app_menu_key_press;
1094     widget_class->button_press_event = hildon_app_menu_button_press;
1095     widget_class->button_release_event = hildon_app_menu_button_release;
1096     widget_class->style_set = hildon_app_menu_style_set;
1097     widget_class->delete_event = hildon_app_menu_delete_event_handler;
1098     widget_class->size_request = hildon_app_menu_size_request;
1099
1100     g_type_class_add_private (klass, sizeof (HildonAppMenuPrivate));
1101
1102     gtk_widget_class_install_style_property (
1103         widget_class,
1104         g_param_spec_uint (
1105             "horizontal-spacing",
1106             "Horizontal spacing on menu items",
1107             "Horizontal spacing between each menu item. Does not apply to filter buttons.",
1108             0, G_MAXUINT, 16,
1109             G_PARAM_READABLE));
1110
1111     gtk_widget_class_install_style_property (
1112         widget_class,
1113         g_param_spec_uint (
1114             "vertical-spacing",
1115             "Vertical spacing on menu items",
1116             "Vertical spacing between each menu item. Does not apply to filter buttons.",
1117             0, G_MAXUINT, 16,
1118             G_PARAM_READABLE));
1119
1120     gtk_widget_class_install_style_property (
1121         widget_class,
1122         g_param_spec_uint (
1123             "filter-vertical-spacing",
1124             "Vertical spacing between filters and menu items",
1125             "Vertical spacing between filters and menu items",
1126             0, G_MAXUINT, 8,
1127             G_PARAM_READABLE));
1128
1129     gtk_widget_class_install_style_property (
1130         widget_class,
1131         g_param_spec_uint (
1132             "inner-border",
1133             "Border between menu edges and buttons",
1134             "Border between menu edges and buttons",
1135             0, G_MAXUINT, 16,
1136             G_PARAM_READABLE));
1137
1138     gtk_widget_class_install_style_property (
1139         widget_class,
1140         g_param_spec_uint (
1141             "external-border",
1142             "Border between menu and screen edges (in horizontal mode)",
1143             "Border between the right and left edges of the menu and "
1144             "the screen edges (in horizontal mode)",
1145             0, G_MAXUINT, 50,
1146             G_PARAM_READABLE));
1147 }