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