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