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