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