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