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