Reviewed release button logic
[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     priv->find_intruder_idle_id = gdk_threads_add_idle (hildon_app_menu_find_intruder, widget);
559 }
560
561 static void
562 hildon_app_menu_unmap                           (GtkWidget *widget)
563 {
564     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
565
566     /* Remove the grab */
567     if (priv->transfer_window != NULL) {
568         gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
569                                     GDK_CURRENT_TIME);
570         gtk_grab_remove (widget);
571
572         gdk_window_destroy (priv->transfer_window);
573         priv->transfer_window = NULL;
574     }
575
576     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unmap (widget);
577
578     gtk_window_set_is_temporary (GTK_WINDOW (widget), FALSE);
579 }
580
581 static void
582 hildon_app_menu_grab_notify                     (GtkWidget *widget,
583                                                  gboolean   was_grabbed)
584 {
585     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->grab_notify)
586         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->grab_notify (widget, was_grabbed);
587
588     if (!was_grabbed && GTK_WIDGET_VISIBLE (widget))
589         gtk_widget_hide (widget);
590 }
591
592 static gboolean
593 hildon_app_menu_hide_idle                       (gpointer widget)
594 {
595     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
596     gtk_widget_hide (GTK_WIDGET (widget));
597     priv->hide_idle_id = 0;
598     return FALSE;
599 }
600
601 /* Send keyboard accelerators to the parent window, if necessary.
602  * This code is heavily based on gtk_menu_key_press ()
603  */
604 static gboolean
605 hildon_app_menu_key_press                       (GtkWidget   *widget,
606                                                  GdkEventKey *event)
607 {
608     GtkWindow *parent_window;
609     HildonAppMenuPrivate *priv;
610
611     g_return_val_if_fail (HILDON_IS_APP_MENU (widget), FALSE);
612     g_return_val_if_fail (event != NULL, FALSE);
613
614     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->key_press_event (widget, event))
615         return TRUE;
616
617     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
618     parent_window = priv->parent_window;
619
620     if (parent_window) {
621         guint accel_key, accel_mods;
622         GdkModifierType consumed_modifiers;
623         GdkDisplay *display;
624         GSList *accel_groups;
625         GSList *list;
626
627         display = gtk_widget_get_display (widget);
628
629         /* Figure out what modifiers went into determining the key symbol */
630         gdk_keymap_translate_keyboard_state (gdk_keymap_get_for_display (display),
631                                              event->hardware_keycode, event->state, event->group,
632                                              NULL, NULL, NULL, &consumed_modifiers);
633
634         accel_key = gdk_keyval_to_lower (event->keyval);
635         accel_mods = event->state & gtk_accelerator_get_default_mod_mask () & ~consumed_modifiers;
636
637         /* If lowercasing affects the keysym, then we need to include SHIFT in the modifiers,
638          * We re-upper case when we match against the keyval, but display and save in caseless form.
639          */
640         if (accel_key != event->keyval)
641             accel_mods |= GDK_SHIFT_MASK;
642
643         accel_groups = gtk_accel_groups_from_object (G_OBJECT (parent_window));
644
645         for (list = accel_groups; list; list = list->next) {
646             GtkAccelGroup *accel_group = list->data;
647
648             if (gtk_accel_group_query (accel_group, accel_key, accel_mods, NULL)) {
649                 gtk_window_activate_key (parent_window, event);
650                 priv->hide_idle_id = gdk_threads_add_idle (hildon_app_menu_hide_idle, widget);
651                 break;
652             }
653         }
654     }
655
656     return TRUE;
657 }
658
659 static gboolean
660 hildon_app_menu_button_press                    (GtkWidget *widget,
661                                                  GdkEventButton *event)
662 {
663     int x, y;
664     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
665
666     gdk_window_get_position (widget->window, &x, &y);
667
668     /* Whether the button has been pressed outside the widget */
669     priv->pressed_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
670                              event->y_root < y || event->y_root > y + widget->allocation.height);
671
672     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event) {
673         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event (widget, event);
674     } else {
675         return FALSE;
676     }
677 }
678
679 static gboolean
680 hildon_app_menu_button_release                  (GtkWidget *widget,
681                                                  GdkEventButton *event)
682 {
683     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
684
685     if (priv->pressed_outside) {
686         int x, y;
687         gboolean released_outside;
688
689         gdk_window_get_position (widget->window, &x, &y);
690
691         /* Whether the button has been released outside the widget */
692         released_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
693                             event->y_root < y || event->y_root > y + widget->allocation.height);
694
695         if (released_outside) {
696             gtk_widget_hide (widget);
697         }
698
699         priv->pressed_outside = FALSE; /* Always reset pressed_outside to FALSE */
700     }
701
702     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event) {
703         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event (widget, event);
704     } else {
705         return FALSE;
706     }
707 }
708
709 static gboolean
710 hildon_app_menu_delete_event_handler            (GtkWidget   *widget,
711                                                  GdkEventAny *event)
712 {
713     /* Hide the menu if it receives a delete-event, but don't destroy it */
714     gtk_widget_hide (widget);
715     return TRUE;
716 }
717
718 /* Grab transfer window (based on the one from GtkMenu) */
719 static GdkWindow *
720 grab_transfer_window_get                        (GtkWidget *widget)
721 {
722     GdkWindow *window;
723     GdkWindowAttr attributes;
724     gint attributes_mask;
725
726     attributes.x = 0;
727     attributes.y = 0;
728     attributes.width = 10;
729     attributes.height = 10;
730     attributes.window_type = GDK_WINDOW_TEMP;
731     attributes.wclass = GDK_INPUT_ONLY;
732     attributes.override_redirect = TRUE;
733     attributes.event_mask = 0;
734
735     attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_NOREDIR;
736
737     window = gdk_window_new (gtk_widget_get_root_window (widget),
738                                  &attributes, attributes_mask);
739     gdk_window_set_user_data (window, widget);
740
741     gdk_window_show (window);
742
743     return window;
744 }
745
746 static void
747 hildon_app_menu_size_request                    (GtkWidget      *widget,
748                                                  GtkRequisition *requisition)
749 {
750     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
751
752     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->size_request (widget, requisition);
753
754     requisition->width = priv->width_request;
755 }
756
757 static void
758 hildon_app_menu_realize                         (GtkWidget *widget)
759 {
760     Atom property, window_type;
761     Display *xdisplay;
762     GdkDisplay *gdkdisplay;
763     GdkScreen *screen;
764
765     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->realize (widget);
766
767     gdk_window_set_decorations (widget->window, GDK_DECOR_BORDER);
768
769     gdkdisplay = gdk_drawable_get_display (widget->window);
770     xdisplay = GDK_WINDOW_XDISPLAY (widget->window);
771
772     property = gdk_x11_get_xatom_by_name_for_display (gdkdisplay, "_NET_WM_WINDOW_TYPE");
773     window_type = XInternAtom (xdisplay, "_HILDON_WM_WINDOW_TYPE_APP_MENU", False);
774     XChangeProperty (xdisplay, GDK_WINDOW_XID (widget->window), property,
775                      XA_ATOM, 32, PropModeReplace, (guchar *) &window_type, 1);
776
777     /* Detect any screen changes */
778     screen = gtk_widget_get_screen (widget);
779     g_signal_connect (screen, "size-changed", G_CALLBACK (screen_size_changed), widget);
780
781     /* Force menu to set the initial layout */
782     screen_size_changed (screen, HILDON_APP_MENU (widget));
783 }
784
785 static void
786 hildon_app_menu_unrealize                       (GtkWidget *widget)
787 {
788     GdkScreen *screen = gtk_widget_get_screen (widget);
789     /* Disconnect "size-changed" signal handler */
790     g_signal_handlers_disconnect_by_func (screen, G_CALLBACK (screen_size_changed), widget);
791
792     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unrealize (widget);
793 }
794
795 static void
796 hildon_app_menu_apply_style                     (GtkWidget *widget)
797 {
798     GdkScreen *screen;
799     guint horizontal_spacing, vertical_spacing, filter_vertical_spacing;
800     guint inner_border, external_border;
801     HildonAppMenuPrivate *priv;
802
803     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
804
805     gtk_widget_style_get (widget,
806                           "horizontal-spacing", &horizontal_spacing,
807                           "vertical-spacing", &vertical_spacing,
808                           "filter-vertical-spacing", &filter_vertical_spacing,
809                           "inner-border", &inner_border,
810                           "external-border", &external_border,
811                           NULL);
812
813     /* Set spacings */
814     gtk_table_set_row_spacings (priv->table, vertical_spacing);
815     gtk_table_set_col_spacings (priv->table, horizontal_spacing);
816     gtk_box_set_spacing (priv->vbox, filter_vertical_spacing);
817
818     /* Set inner border */
819     gtk_container_set_border_width (GTK_CONTAINER (widget), inner_border);
820
821     /* Compute width request */
822     screen = gtk_widget_get_screen (widget);
823     if (gdk_screen_get_width (screen) < gdk_screen_get_height (screen)) {
824         external_border = 0;
825     }
826     priv->width_request = gdk_screen_get_width (screen) - external_border * 2;
827     gtk_window_move (GTK_WINDOW (widget), external_border, 0);
828     gtk_widget_queue_resize (widget);
829 }
830
831 static void
832 hildon_app_menu_style_set                       (GtkWidget *widget,
833                                                  GtkStyle  *previous_style)
834 {
835     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set)
836         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set (widget, previous_style);
837
838     hildon_app_menu_apply_style (widget);
839 }
840
841 static void
842 hildon_app_menu_repack_filters                  (HildonAppMenu *menu)
843 {
844     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
845     GList *iter;
846
847     for (iter = priv->filters; iter != NULL; iter = iter->next) {
848         GtkWidget *filter = GTK_WIDGET (iter->data);
849         GtkWidget *parent = gtk_widget_get_parent (filter);
850         if (parent) {
851             g_object_ref (filter);
852             gtk_container_remove (GTK_CONTAINER (parent), filter);
853         }
854     }
855
856     for (iter = priv->filters; iter != NULL; iter = iter->next) {
857         GtkWidget *filter = GTK_WIDGET (iter->data);
858         if (GTK_WIDGET_VISIBLE (filter)) {
859             gtk_box_pack_start (GTK_BOX (priv->filters_hbox), filter, TRUE, TRUE, 0);
860             g_object_unref (filter);
861             /* GtkButton must be realized for accelerators to work */
862             gtk_widget_realize (filter);
863         }
864     }
865 }
866
867 /*
868  * When items displayed in the menu change (e.g, a new item is added,
869  * an item is hidden or the list is reordered), the layout must be
870  * updated. To do this we repack all items starting from a given one.
871  */
872 static void
873 hildon_app_menu_repack_items                    (HildonAppMenu *menu,
874                                                  gint           start_from)
875 {
876     HildonAppMenuPrivate *priv;
877     gint row, col, nvisible, i;
878     GList *iter;
879
880     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
881
882     i = nvisible = 0;
883     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
884         /* Count number of visible items */
885         if (GTK_WIDGET_VISIBLE (iter->data))
886             nvisible++;
887         /* Remove buttons from their parent */
888         if (start_from != -1 && i >= start_from) {
889             GtkWidget *item = GTK_WIDGET (iter->data);
890             GtkWidget *parent = gtk_widget_get_parent (item);
891             if (parent) {
892                 g_object_ref (item);
893                 gtk_container_remove (GTK_CONTAINER (parent), item);
894             }
895         }
896         i++;
897     }
898
899     /* If items have been removed, recalculate the size of the menu */
900     if (start_from != -1)
901         gtk_window_resize (GTK_WINDOW (menu), 1, 1);
902
903     /* Set the final size now to avoid unnecessary resizes later */
904     if (nvisible > 0)
905         gtk_table_resize (priv->table, ((nvisible - 1) / priv->columns) + 1, priv->columns);
906
907     /* Add buttons */
908     row = col = 0;
909     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
910         GtkWidget *item = GTK_WIDGET (iter->data);
911         if (GTK_WIDGET_VISIBLE (item)) {
912             /* Don't add an item to the table if it's already there */
913             if (gtk_widget_get_parent (item) == NULL) {
914                 gtk_table_attach_defaults (priv->table, item, col, col + 1, row, row + 1);
915                 g_object_unref (item);
916                 /* GtkButton must be realized for accelerators to work */
917                 gtk_widget_realize (item);
918             }
919             if (++col == priv->columns) {
920                 col = 0;
921                 row++;
922             }
923         }
924     }
925 }
926
927 /**
928  * hildon_app_menu_popup:
929  * @menu: a #HildonAppMenu
930  * @parent_window: a #GtkWindow
931  *
932  * Displays a menu on top of a window and makes it available for
933  * selection.
934  *
935  * Since: 2.2
936  **/
937 void
938 hildon_app_menu_popup                           (HildonAppMenu *menu,
939                                                  GtkWindow     *parent_window)
940 {
941     HildonAppMenuPrivate *priv;
942     gboolean show_menu = FALSE;
943     GList *i;
944
945     g_return_if_fail (HILDON_IS_APP_MENU (menu));
946     g_return_if_fail (GTK_IS_WINDOW (parent_window));
947
948     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
949
950     /* Don't show menu if it doesn't contain visible items */
951     for (i = priv->buttons; i && !show_menu; i = i->next)
952         show_menu = GTK_WIDGET_VISIBLE (i->data);
953
954     for (i = priv->filters; i && !show_menu; i = i->next)
955         show_menu = GTK_WIDGET_VISIBLE (i->data);
956
957     if (show_menu) {
958         hildon_app_menu_set_parent_window (menu, parent_window);
959         gtk_widget_show (GTK_WIDGET (menu));
960     }
961
962 }
963
964 /**
965  * hildon_app_menu_get_items:
966  * @menu: a #HildonAppMenu
967  *
968  * Returns a list of all items (regular items, not filters) contained
969  * in @menu.
970  *
971  * Returns: a newly-allocated list containing the items in @menu
972  *
973  * Since: 2.2
974  **/
975 GList *
976 hildon_app_menu_get_items                       (HildonAppMenu *menu)
977 {
978     HildonAppMenuPrivate *priv;
979
980     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
981
982     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
983
984     return g_list_copy (priv->buttons);
985 }
986
987 /**
988  * hildon_app_menu_get_filters:
989  * @menu: a #HildonAppMenu
990  *
991  * Returns a list of all filters contained in @menu.
992  *
993  * Returns: a newly-allocated list containing the filters in @menu
994  *
995  * Since: 2.2
996  **/
997 GList *
998 hildon_app_menu_get_filters                     (HildonAppMenu *menu)
999 {
1000     HildonAppMenuPrivate *priv;
1001
1002     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
1003
1004     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
1005
1006     return g_list_copy (priv->filters);
1007 }
1008
1009 static void
1010 hildon_app_menu_init                            (HildonAppMenu *menu)
1011 {
1012     GtkWidget *alignment;
1013     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
1014
1015     /* Initialize private variables */
1016     priv->parent_window = NULL;
1017     priv->transfer_window = NULL;
1018     priv->pressed_outside = FALSE;
1019     priv->inhibit_repack = FALSE;
1020     priv->buttons = NULL;
1021     priv->filters = NULL;
1022     priv->columns = 2;
1023     priv->width_request = -1;
1024     priv->find_intruder_idle_id = 0;
1025     priv->hide_idle_id = 0;
1026
1027     /* Create boxes and tables */
1028     priv->filters_hbox = GTK_BOX (gtk_hbox_new (TRUE, 0));
1029     priv->vbox = GTK_BOX (gtk_vbox_new (FALSE, 0));
1030     priv->table = GTK_TABLE (gtk_table_new (1, priv->columns, TRUE));
1031
1032     /* Align the filters to the center */
1033     alignment = gtk_alignment_new (0.5, 0.5, 0, 0);
1034     gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (priv->filters_hbox));
1035
1036     /* Pack everything */
1037     gtk_container_add (GTK_CONTAINER (menu), GTK_WIDGET (priv->vbox));
1038     gtk_box_pack_start (priv->vbox, alignment, TRUE, TRUE, 0);
1039     gtk_box_pack_start (priv->vbox, GTK_WIDGET (priv->table), TRUE, TRUE, 0);
1040
1041     /* This should be treated like a normal, ref-counted widget */
1042     g_object_force_floating (G_OBJECT (menu));
1043     GTK_WINDOW (menu)->has_user_ref_count = FALSE;
1044
1045     gtk_widget_show_all (GTK_WIDGET (priv->vbox));
1046 }
1047
1048 static void
1049 hildon_app_menu_finalize                        (GObject *object)
1050 {
1051     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(object);
1052
1053     if (priv->find_intruder_idle_id) {
1054         g_source_remove (priv->find_intruder_idle_id);
1055         priv->find_intruder_idle_id = 0;
1056     }
1057
1058     if (priv->hide_idle_id) {
1059         g_source_remove (priv->hide_idle_id);
1060         priv->hide_idle_id = 0;
1061     }
1062
1063     if (priv->parent_window) {
1064         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_topmost_notify, object);
1065         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_unmapped, object);
1066     }
1067
1068     if (priv->transfer_window)
1069         gdk_window_destroy (priv->transfer_window);
1070
1071     g_list_foreach (priv->buttons, (GFunc) g_object_unref, NULL);
1072     g_list_foreach (priv->filters, (GFunc) g_object_unref, NULL);
1073
1074     g_list_free (priv->buttons);
1075     g_list_free (priv->filters);
1076
1077     g_signal_handlers_destroy (object);
1078     G_OBJECT_CLASS (hildon_app_menu_parent_class)->finalize (object);
1079 }
1080
1081 static void
1082 hildon_app_menu_class_init                      (HildonAppMenuClass *klass)
1083 {
1084     GObjectClass *gobject_class = (GObjectClass *)klass;
1085     GtkWidgetClass *widget_class = (GtkWidgetClass *)klass;
1086
1087     gobject_class->finalize = hildon_app_menu_finalize;
1088     widget_class->show_all = hildon_app_menu_show_all;
1089     widget_class->hide_all = hildon_app_menu_hide_all;
1090     widget_class->map = hildon_app_menu_map;
1091     widget_class->unmap = hildon_app_menu_unmap;
1092     widget_class->realize = hildon_app_menu_realize;
1093     widget_class->unrealize = hildon_app_menu_unrealize;
1094     widget_class->grab_notify = hildon_app_menu_grab_notify;
1095     widget_class->key_press_event = hildon_app_menu_key_press;
1096     widget_class->button_press_event = hildon_app_menu_button_press;
1097     widget_class->button_release_event = hildon_app_menu_button_release;
1098     widget_class->style_set = hildon_app_menu_style_set;
1099     widget_class->delete_event = hildon_app_menu_delete_event_handler;
1100     widget_class->size_request = hildon_app_menu_size_request;
1101
1102     g_type_class_add_private (klass, sizeof (HildonAppMenuPrivate));
1103
1104     gtk_widget_class_install_style_property (
1105         widget_class,
1106         g_param_spec_uint (
1107             "horizontal-spacing",
1108             "Horizontal spacing on menu items",
1109             "Horizontal spacing between each menu item. Does not apply to filter buttons.",
1110             0, G_MAXUINT, 16,
1111             G_PARAM_READABLE));
1112
1113     gtk_widget_class_install_style_property (
1114         widget_class,
1115         g_param_spec_uint (
1116             "vertical-spacing",
1117             "Vertical spacing on menu items",
1118             "Vertical spacing between each menu item. Does not apply to filter buttons.",
1119             0, G_MAXUINT, 16,
1120             G_PARAM_READABLE));
1121
1122     gtk_widget_class_install_style_property (
1123         widget_class,
1124         g_param_spec_uint (
1125             "filter-vertical-spacing",
1126             "Vertical spacing between filters and menu items",
1127             "Vertical spacing between filters and menu items",
1128             0, G_MAXUINT, 8,
1129             G_PARAM_READABLE));
1130
1131     gtk_widget_class_install_style_property (
1132         widget_class,
1133         g_param_spec_uint (
1134             "inner-border",
1135             "Border between menu edges and buttons",
1136             "Border between menu edges and buttons",
1137             0, G_MAXUINT, 16,
1138             G_PARAM_READABLE));
1139
1140     gtk_widget_class_install_style_property (
1141         widget_class,
1142         g_param_spec_uint (
1143             "external-border",
1144             "Border between menu and screen edges (in horizontal mode)",
1145             "Border between the right and left edges of the menu and "
1146             "the screen edges (in horizontal mode)",
1147             0, G_MAXUINT, 50,
1148             G_PARAM_READABLE));
1149 }