Remove leftover function declaration
[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 gboolean
527 hildon_app_menu_hide_idle                       (gpointer widget)
528 {
529     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
530     gtk_widget_hide (GTK_WIDGET (widget));
531     priv->hide_idle_id = 0;
532     return FALSE;
533 }
534
535 /* Send keyboard accelerators to the parent window, if necessary.
536  * This code is heavily based on gtk_menu_key_press ()
537  */
538 static gboolean
539 hildon_app_menu_key_press                       (GtkWidget   *widget,
540                                                  GdkEventKey *event)
541 {
542     GtkWindow *parent_window;
543     HildonAppMenuPrivate *priv;
544
545     g_return_val_if_fail (HILDON_IS_APP_MENU (widget), FALSE);
546     g_return_val_if_fail (event != NULL, FALSE);
547
548     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->key_press_event (widget, event))
549         return TRUE;
550
551     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
552     parent_window = priv->parent_window;
553
554     if (parent_window) {
555         guint accel_key, accel_mods;
556         GdkModifierType consumed_modifiers;
557         GdkDisplay *display;
558         GSList *accel_groups;
559         GSList *list;
560
561         display = gtk_widget_get_display (widget);
562
563         /* Figure out what modifiers went into determining the key symbol */
564         gdk_keymap_translate_keyboard_state (gdk_keymap_get_for_display (display),
565                                              event->hardware_keycode, event->state, event->group,
566                                              NULL, NULL, NULL, &consumed_modifiers);
567
568         accel_key = gdk_keyval_to_lower (event->keyval);
569         accel_mods = event->state & gtk_accelerator_get_default_mod_mask () & ~consumed_modifiers;
570
571         /* If lowercasing affects the keysym, then we need to include SHIFT in the modifiers,
572          * We re-upper case when we match against the keyval, but display and save in caseless form.
573          */
574         if (accel_key != event->keyval)
575             accel_mods |= GDK_SHIFT_MASK;
576
577         accel_groups = gtk_accel_groups_from_object (G_OBJECT (parent_window));
578
579         for (list = accel_groups; list; list = list->next) {
580             GtkAccelGroup *accel_group = list->data;
581
582             if (gtk_accel_group_query (accel_group, accel_key, accel_mods, NULL)) {
583                 gtk_window_activate_key (parent_window, event);
584                 priv->hide_idle_id = gdk_threads_add_idle (hildon_app_menu_hide_idle, widget);
585                 break;
586             }
587         }
588     }
589
590     return TRUE;
591 }
592
593 static gboolean
594 hildon_app_menu_delete_event_handler            (GtkWidget   *widget,
595                                                  GdkEventAny *event)
596 {
597     /* Hide the menu if it receives a delete-event, but don't destroy it */
598     gtk_widget_hide (widget);
599     return TRUE;
600 }
601
602 static void
603 hildon_app_menu_size_request                    (GtkWidget      *widget,
604                                                  GtkRequisition *requisition)
605 {
606     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
607
608     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->size_request (widget, requisition);
609
610     requisition->width = priv->width_request;
611 }
612
613 static void
614 hildon_app_menu_realize                         (GtkWidget *widget)
615 {
616     Atom property, window_type;
617     Display *xdisplay;
618     GdkDisplay *gdkdisplay;
619     GdkScreen *screen;
620
621     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->realize (widget);
622
623     gdk_window_set_decorations (widget->window, GDK_DECOR_BORDER);
624
625     gdkdisplay = gdk_drawable_get_display (widget->window);
626     xdisplay = GDK_WINDOW_XDISPLAY (widget->window);
627
628     property = gdk_x11_get_xatom_by_name_for_display (gdkdisplay, "_NET_WM_WINDOW_TYPE");
629     window_type = XInternAtom (xdisplay, "_HILDON_WM_WINDOW_TYPE_APP_MENU", False);
630     XChangeProperty (xdisplay, GDK_WINDOW_XID (widget->window), property,
631                      XA_ATOM, 32, PropModeReplace, (guchar *) &window_type, 1);
632
633     /* Detect any screen changes */
634     screen = gtk_widget_get_screen (widget);
635     g_signal_connect (screen, "size-changed", G_CALLBACK (screen_size_changed), widget);
636
637     /* Force menu to set the initial layout */
638     screen_size_changed (screen, HILDON_APP_MENU (widget));
639 }
640
641 static void
642 hildon_app_menu_unrealize                       (GtkWidget *widget)
643 {
644     GdkScreen *screen = gtk_widget_get_screen (widget);
645     /* Disconnect "size-changed" signal handler */
646     g_signal_handlers_disconnect_by_func (screen, G_CALLBACK (screen_size_changed), widget);
647
648     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unrealize (widget);
649 }
650
651 static void
652 hildon_app_menu_apply_style                     (GtkWidget *widget)
653 {
654     GdkScreen *screen;
655     gint filter_group_width;
656     guint horizontal_spacing, vertical_spacing, filter_vertical_spacing;
657     guint inner_border, external_border;
658     HildonAppMenuPrivate *priv;
659
660     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
661
662     gtk_widget_style_get (widget,
663                           "horizontal-spacing", &horizontal_spacing,
664                           "vertical-spacing", &vertical_spacing,
665                           "filter-group-width", &filter_group_width,
666                           "filter-vertical-spacing", &filter_vertical_spacing,
667                           "inner-border", &inner_border,
668                           "external-border", &external_border,
669                           NULL);
670
671     /* Set spacings */
672     gtk_table_set_row_spacings (priv->table, vertical_spacing);
673     gtk_table_set_col_spacings (priv->table, horizontal_spacing);
674     gtk_box_set_spacing (priv->vbox, filter_vertical_spacing);
675
676     /* Set inner border */
677     gtk_container_set_border_width (GTK_CONTAINER (widget), inner_border);
678
679     /* Set width of the group of filter buttons */
680     gtk_widget_set_size_request (GTK_WIDGET (priv->filters_hbox), filter_group_width, -1);
681
682     /* Compute width request */
683     screen = gtk_widget_get_screen (widget);
684     if (gdk_screen_get_width (screen) < gdk_screen_get_height (screen)) {
685         external_border = 0;
686     }
687     priv->width_request = gdk_screen_get_width (screen) - external_border * 2;
688
689     if (widget->window)
690       gdk_window_move_resize (widget->window,
691                               external_border, 0, 1, 1);
692
693     gtk_widget_queue_resize (widget);
694 }
695
696 static void
697 hildon_app_menu_style_set                       (GtkWidget *widget,
698                                                  GtkStyle  *previous_style)
699 {
700     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set)
701         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set (widget, previous_style);
702
703     hildon_app_menu_apply_style (widget);
704 }
705
706 static void
707 hildon_app_menu_repack_filters                  (HildonAppMenu *menu)
708 {
709     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
710     GList *iter;
711
712     for (iter = priv->filters; iter != NULL; iter = iter->next) {
713         GtkWidget *filter = GTK_WIDGET (iter->data);
714         GtkWidget *parent = gtk_widget_get_parent (filter);
715         if (parent) {
716             g_object_ref (filter);
717             gtk_container_remove (GTK_CONTAINER (parent), filter);
718         }
719     }
720
721     for (iter = priv->filters; iter != NULL; iter = iter->next) {
722         GtkWidget *filter = GTK_WIDGET (iter->data);
723         if (GTK_WIDGET_VISIBLE (filter)) {
724             gtk_box_pack_start (GTK_BOX (priv->filters_hbox), filter, TRUE, TRUE, 0);
725             g_object_unref (filter);
726             /* GtkButton must be realized for accelerators to work */
727             gtk_widget_realize (filter);
728         }
729     }
730 }
731
732 /*
733  * When items displayed in the menu change (e.g, a new item is added,
734  * an item is hidden or the list is reordered), the layout must be
735  * updated. To do this we repack all items starting from a given one.
736  */
737 static void
738 hildon_app_menu_repack_items                    (HildonAppMenu *menu,
739                                                  gint           start_from)
740 {
741     HildonAppMenuPrivate *priv;
742     gint row, col, nvisible, i;
743     GList *iter;
744
745     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
746
747     i = nvisible = 0;
748     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
749         /* Count number of visible items */
750         if (GTK_WIDGET_VISIBLE (iter->data))
751             nvisible++;
752         /* Remove buttons from their parent */
753         if (start_from != -1 && i >= start_from) {
754             GtkWidget *item = GTK_WIDGET (iter->data);
755             GtkWidget *parent = gtk_widget_get_parent (item);
756             if (parent) {
757                 g_object_ref (item);
758                 gtk_container_remove (GTK_CONTAINER (parent), item);
759             }
760         }
761         i++;
762     }
763
764     /* If items have been removed, recalculate the size of the menu */
765     if (start_from != -1)
766         gtk_window_resize (GTK_WINDOW (menu), 1, 1);
767
768     /* Set the final size now to avoid unnecessary resizes later */
769     if (nvisible > 0)
770         gtk_table_resize (priv->table, ((nvisible - 1) / priv->columns) + 1, priv->columns);
771
772     /* Add buttons */
773     row = col = 0;
774     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
775         GtkWidget *item = GTK_WIDGET (iter->data);
776         if (GTK_WIDGET_VISIBLE (item)) {
777             /* Don't add an item to the table if it's already there */
778             if (gtk_widget_get_parent (item) == NULL) {
779                 gtk_table_attach_defaults (priv->table, item, col, col + 1, row, row + 1);
780                 g_object_unref (item);
781                 /* GtkButton must be realized for accelerators to work */
782                 gtk_widget_realize (item);
783             }
784             if (++col == priv->columns) {
785                 col = 0;
786                 row++;
787             }
788         }
789     }
790 }
791
792 /**
793  * hildon_app_menu_popup:
794  * @menu: a #HildonAppMenu
795  * @parent_window: a #GtkWindow
796  *
797  * Displays a menu on top of a window and makes it available for
798  * selection.
799  *
800  * Since: 2.2
801  **/
802 void
803 hildon_app_menu_popup                           (HildonAppMenu *menu,
804                                                  GtkWindow     *parent_window)
805 {
806     HildonAppMenuPrivate *priv;
807     gboolean show_menu = FALSE;
808     GList *i;
809
810     g_return_if_fail (HILDON_IS_APP_MENU (menu));
811     g_return_if_fail (GTK_IS_WINDOW (parent_window));
812
813     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
814
815     /* Don't show menu if it doesn't contain visible items */
816     for (i = priv->buttons; i && !show_menu; i = i->next)
817         show_menu = GTK_WIDGET_VISIBLE (i->data);
818
819     for (i = priv->filters; i && !show_menu; i = i->next)
820         show_menu = GTK_WIDGET_VISIBLE (i->data);
821
822     if (show_menu) {
823         hildon_app_menu_set_parent_window (menu, parent_window);
824         gtk_widget_show (GTK_WIDGET (menu));
825     }
826
827 }
828
829 /**
830  * hildon_app_menu_get_items:
831  * @menu: a #HildonAppMenu
832  *
833  * Returns a list of all items (regular items, not filters) contained
834  * in @menu.
835  *
836  * Returns: a newly-allocated list containing the items in @menu
837  *
838  * Since: 2.2
839  **/
840 GList *
841 hildon_app_menu_get_items                       (HildonAppMenu *menu)
842 {
843     HildonAppMenuPrivate *priv;
844
845     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
846
847     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
848
849     return g_list_copy (priv->buttons);
850 }
851
852 /**
853  * hildon_app_menu_get_filters:
854  * @menu: a #HildonAppMenu
855  *
856  * Returns a list of all filters contained in @menu.
857  *
858  * Returns: a newly-allocated list containing the filters in @menu
859  *
860  * Since: 2.2
861  **/
862 GList *
863 hildon_app_menu_get_filters                     (HildonAppMenu *menu)
864 {
865     HildonAppMenuPrivate *priv;
866
867     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
868
869     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
870
871     return g_list_copy (priv->filters);
872 }
873
874 static void
875 hildon_app_menu_init                            (HildonAppMenu *menu)
876 {
877     GtkWidget *alignment;
878     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
879
880     /* Initialize private variables */
881     priv->parent_window = NULL;
882     priv->transfer_window = NULL;
883     priv->pressed_outside = FALSE;
884     priv->inhibit_repack = FALSE;
885     priv->last_pressed_button = NULL;
886     priv->buttons = NULL;
887     priv->filters = NULL;
888     priv->columns = 2;
889     priv->width_request = -1;
890     priv->find_intruder_idle_id = 0;
891     priv->hide_idle_id = 0;
892
893     /* Create boxes and tables */
894     priv->filters_hbox = GTK_BOX (gtk_hbox_new (TRUE, 0));
895     priv->vbox = GTK_BOX (gtk_vbox_new (FALSE, 0));
896     priv->table = GTK_TABLE (gtk_table_new (1, priv->columns, TRUE));
897
898     /* Align the filters to the center */
899     alignment = gtk_alignment_new (0.5, 0.5, 0, 0);
900     gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (priv->filters_hbox));
901
902     /* Pack everything */
903     gtk_container_add (GTK_CONTAINER (menu), GTK_WIDGET (priv->vbox));
904     gtk_box_pack_start (priv->vbox, alignment, TRUE, TRUE, 0);
905     gtk_box_pack_start (priv->vbox, GTK_WIDGET (priv->table), TRUE, TRUE, 0);
906
907     /* This should be treated like a normal, ref-counted widget */
908     g_object_force_floating (G_OBJECT (menu));
909     GTK_WINDOW (menu)->has_user_ref_count = FALSE;
910
911     gtk_widget_show_all (GTK_WIDGET (priv->vbox));
912 }
913
914 static void
915 hildon_app_menu_finalize                        (GObject *object)
916 {
917     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(object);
918
919     if (priv->find_intruder_idle_id) {
920         g_source_remove (priv->find_intruder_idle_id);
921         priv->find_intruder_idle_id = 0;
922     }
923
924     if (priv->hide_idle_id) {
925         g_source_remove (priv->hide_idle_id);
926         priv->hide_idle_id = 0;
927     }
928
929     if (priv->parent_window) {
930         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_topmost_notify, object);
931         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_unmapped, object);
932     }
933
934     if (priv->transfer_window)
935         gdk_window_destroy (priv->transfer_window);
936
937     g_list_foreach (priv->buttons, (GFunc) g_object_unref, NULL);
938     g_list_foreach (priv->filters, (GFunc) g_object_unref, NULL);
939
940     g_list_free (priv->buttons);
941     g_list_free (priv->filters);
942
943     g_signal_handlers_destroy (object);
944     G_OBJECT_CLASS (hildon_app_menu_parent_class)->finalize (object);
945 }
946
947 static void
948 hildon_app_menu_class_init                      (HildonAppMenuClass *klass)
949 {
950     GObjectClass *gobject_class = (GObjectClass *)klass;
951     GtkWidgetClass *widget_class = (GtkWidgetClass *)klass;
952
953     gobject_class->finalize = hildon_app_menu_finalize;
954     widget_class->show_all = hildon_app_menu_show_all;
955     widget_class->hide_all = hildon_app_menu_hide_all;
956     widget_class->map = hildon_app_menu_map;
957     widget_class->realize = hildon_app_menu_realize;
958     widget_class->unrealize = hildon_app_menu_unrealize;
959     widget_class->key_press_event = hildon_app_menu_key_press;
960     widget_class->style_set = hildon_app_menu_style_set;
961     widget_class->delete_event = hildon_app_menu_delete_event_handler;
962     widget_class->size_request = hildon_app_menu_size_request;
963
964     g_type_class_add_private (klass, sizeof (HildonAppMenuPrivate));
965
966     gtk_widget_class_install_style_property (
967         widget_class,
968         g_param_spec_uint (
969             "horizontal-spacing",
970             "Horizontal spacing on menu items",
971             "Horizontal spacing between each menu item. Does not apply to filter buttons.",
972             0, G_MAXUINT, 16,
973             G_PARAM_READABLE));
974
975     gtk_widget_class_install_style_property (
976         widget_class,
977         g_param_spec_uint (
978             "vertical-spacing",
979             "Vertical spacing on menu items",
980             "Vertical spacing between each menu item. Does not apply to filter buttons.",
981             0, G_MAXUINT, 16,
982             G_PARAM_READABLE));
983
984     gtk_widget_class_install_style_property (
985         widget_class,
986         g_param_spec_int (
987             "filter-group-width",
988             "Width of the group of filter buttons",
989             "Total width of the group of filter buttons, "
990             "or -1 to use the natural size request.",
991             -1, G_MAXINT, 444,
992             G_PARAM_READABLE));
993
994     gtk_widget_class_install_style_property (
995         widget_class,
996         g_param_spec_uint (
997             "filter-vertical-spacing",
998             "Vertical spacing between filters and menu items",
999             "Vertical spacing between filters and menu items",
1000             0, G_MAXUINT, 8,
1001             G_PARAM_READABLE));
1002
1003     gtk_widget_class_install_style_property (
1004         widget_class,
1005         g_param_spec_uint (
1006             "inner-border",
1007             "Border between menu edges and buttons",
1008             "Border between menu edges and buttons",
1009             0, G_MAXUINT, 16,
1010             G_PARAM_READABLE));
1011
1012     gtk_widget_class_install_style_property (
1013         widget_class,
1014         g_param_spec_uint (
1015             "external-border",
1016             "Border between menu and screen edges (in horizontal mode)",
1017             "Border between the right and left edges of the menu and "
1018             "the screen edges (in horizontal mode)",
1019             0, G_MAXUINT, 50,
1020             G_PARAM_READABLE));
1021 }