Add HildonAppMenu::changed signal
[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 }
429
430 static void
431 filter_visibility_changed                       (GtkWidget     *item,
432                                                  GParamSpec    *arg1,
433                                                  HildonAppMenu *menu)
434 {
435     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (menu);
436
437     if (! priv->inhibit_repack)
438         hildon_app_menu_repack_filters (menu);
439 }
440
441 static void
442 remove_item_from_list                           (GList    **list,
443                                                  gpointer   item)
444 {
445     *list = g_list_remove (*list, item);
446 }
447
448 static void
449 emit_menu_changed                               (HildonAppMenu *menu,
450                                                  gpointer item)
451 {
452     g_signal_emit (menu, app_menu_signals[CHANGED], 0);
453 }
454
455 static void
456 hildon_app_menu_show_all                        (GtkWidget *widget)
457 {
458     HildonAppMenu *menu = HILDON_APP_MENU (widget);
459     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
460
461     priv->inhibit_repack = TRUE;
462
463     /* Show children, but not self. */
464     g_list_foreach (priv->buttons, (GFunc) gtk_widget_show_all, NULL);
465     g_list_foreach (priv->filters, (GFunc) gtk_widget_show_all, NULL);
466
467     priv->inhibit_repack = FALSE;
468
469     hildon_app_menu_repack_items (menu, 0);
470     hildon_app_menu_repack_filters (menu);
471 }
472
473
474 static void
475 hildon_app_menu_hide_all                        (GtkWidget *widget)
476 {
477     HildonAppMenu *menu = HILDON_APP_MENU (widget);
478     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
479
480     priv->inhibit_repack = TRUE;
481
482     /* Hide children, but not self. */
483     g_list_foreach (priv->buttons, (GFunc) gtk_widget_hide_all, NULL);
484     g_list_foreach (priv->filters, (GFunc) gtk_widget_hide_all, NULL);
485
486     priv->inhibit_repack = FALSE;
487
488     hildon_app_menu_repack_items (menu, 0);
489     hildon_app_menu_repack_filters (menu);
490 }
491
492 /*
493  * There's a race condition that can freeze the UI if a dialog appears
494  * between a HildonAppMenu and its parent window, see NB#100468
495  */
496 static gboolean
497 hildon_app_menu_find_intruder                   (gpointer data)
498 {
499     GtkWidget *widget = GTK_WIDGET (data);
500     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
501
502     priv->find_intruder_idle_id = 0;
503
504     /* If there's a window between the menu and its parent window, hide the menu */
505     if (priv->parent_window) {
506         gboolean intruder_found = FALSE;
507         GdkScreen *screen = gtk_widget_get_screen (widget);
508         GList *stack = gdk_screen_get_window_stack (screen);
509         GList *parent_pos = g_list_find (stack, GTK_WIDGET (priv->parent_window)->window);
510         GList *toplevels = gtk_window_list_toplevels ();
511         GList *i;
512
513         for (i = toplevels; i != NULL && !intruder_found; i = i->next) {
514             if (i->data != widget && i->data != priv->parent_window) {
515                 if (g_list_find (parent_pos, GTK_WIDGET (i->data)->window)) {
516                     /* HildonBanners are not closed automatically when
517                      * a new window appears, so we must close them by
518                      * hand to make the AppMenu work as expected.
519                      * Yes, this is a hack. See NB#111027 */
520                     if (HILDON_IS_BANNER (i->data)) {
521                         gtk_widget_hide (i->data);
522                     } else if (!HILDON_IS_ANIMATION_ACTOR (i->data)) {
523                         intruder_found = TRUE;
524                     }
525                 }
526             }
527         }
528
529         g_list_foreach (stack, (GFunc) g_object_unref, NULL);
530         g_list_free (stack);
531         g_list_free (toplevels);
532
533         if (intruder_found)
534             gtk_widget_hide (widget);
535     }
536
537     return FALSE;
538 }
539
540 static void
541 hildon_app_menu_map                             (GtkWidget *widget)
542 {
543     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
544
545     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->map (widget);
546
547     if (priv->find_intruder_idle_id == 0)
548         priv->find_intruder_idle_id = gdk_threads_add_idle (hildon_app_menu_find_intruder, widget);
549 }
550
551 static void
552 hildon_app_menu_grab_notify                     (GtkWidget *widget,
553                                                  gboolean   was_grabbed)
554 {
555     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->grab_notify)
556         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->grab_notify (widget, was_grabbed);
557
558     if (!was_grabbed && GTK_WIDGET_VISIBLE (widget))
559         gtk_widget_hide (widget);
560 }
561
562 static gboolean
563 hildon_app_menu_hide_idle                       (gpointer widget)
564 {
565     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
566     gtk_widget_hide (GTK_WIDGET (widget));
567     priv->hide_idle_id = 0;
568     return FALSE;
569 }
570
571 /* Send keyboard accelerators to the parent window, if necessary.
572  * This code is heavily based on gtk_menu_key_press ()
573  */
574 static gboolean
575 hildon_app_menu_key_press                       (GtkWidget   *widget,
576                                                  GdkEventKey *event)
577 {
578     GtkWindow *parent_window;
579     HildonAppMenuPrivate *priv;
580
581     g_return_val_if_fail (HILDON_IS_APP_MENU (widget), FALSE);
582     g_return_val_if_fail (event != NULL, FALSE);
583
584     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->key_press_event (widget, event))
585         return TRUE;
586
587     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
588     parent_window = priv->parent_window;
589
590     if (parent_window) {
591         guint accel_key, accel_mods;
592         GdkModifierType consumed_modifiers;
593         GdkDisplay *display;
594         GSList *accel_groups;
595         GSList *list;
596
597         display = gtk_widget_get_display (widget);
598
599         /* Figure out what modifiers went into determining the key symbol */
600         gdk_keymap_translate_keyboard_state (gdk_keymap_get_for_display (display),
601                                              event->hardware_keycode, event->state, event->group,
602                                              NULL, NULL, NULL, &consumed_modifiers);
603
604         accel_key = gdk_keyval_to_lower (event->keyval);
605         accel_mods = event->state & gtk_accelerator_get_default_mod_mask () & ~consumed_modifiers;
606
607         /* If lowercasing affects the keysym, then we need to include SHIFT in the modifiers,
608          * We re-upper case when we match against the keyval, but display and save in caseless form.
609          */
610         if (accel_key != event->keyval)
611             accel_mods |= GDK_SHIFT_MASK;
612
613         accel_groups = gtk_accel_groups_from_object (G_OBJECT (parent_window));
614
615         for (list = accel_groups; list; list = list->next) {
616             GtkAccelGroup *accel_group = list->data;
617
618             if (gtk_accel_group_query (accel_group, accel_key, accel_mods, NULL)) {
619                 gtk_window_activate_key (parent_window, event);
620                 priv->hide_idle_id = gdk_threads_add_idle (hildon_app_menu_hide_idle, widget);
621                 break;
622             }
623         }
624     }
625
626     return TRUE;
627 }
628
629 static gboolean
630 hildon_app_menu_delete_event_handler            (GtkWidget   *widget,
631                                                  GdkEventAny *event)
632 {
633     /* Hide the menu if it receives a delete-event, but don't destroy it */
634     gtk_widget_hide (widget);
635     return TRUE;
636 }
637
638 static void
639 hildon_app_menu_size_request                    (GtkWidget      *widget,
640                                                  GtkRequisition *requisition)
641 {
642     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE (widget);
643
644     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->size_request (widget, requisition);
645
646     requisition->width = priv->width_request;
647 }
648
649 static void
650 hildon_app_menu_realize                         (GtkWidget *widget)
651 {
652     Atom property, window_type;
653     Display *xdisplay;
654     GdkDisplay *gdkdisplay;
655     GdkScreen *screen;
656
657     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->realize (widget);
658
659     gdk_window_set_decorations (widget->window, GDK_DECOR_BORDER);
660
661     gdkdisplay = gdk_drawable_get_display (widget->window);
662     xdisplay = GDK_WINDOW_XDISPLAY (widget->window);
663
664     property = gdk_x11_get_xatom_by_name_for_display (gdkdisplay, "_NET_WM_WINDOW_TYPE");
665     window_type = XInternAtom (xdisplay, "_HILDON_WM_WINDOW_TYPE_APP_MENU", False);
666     XChangeProperty (xdisplay, GDK_WINDOW_XID (widget->window), property,
667                      XA_ATOM, 32, PropModeReplace, (guchar *) &window_type, 1);
668
669     /* Detect any screen changes */
670     screen = gtk_widget_get_screen (widget);
671     g_signal_connect (screen, "size-changed", G_CALLBACK (screen_size_changed), widget);
672
673     /* Force menu to set the initial layout */
674     screen_size_changed (screen, HILDON_APP_MENU (widget));
675 }
676
677 static void
678 hildon_app_menu_unrealize                       (GtkWidget *widget)
679 {
680     GdkScreen *screen = gtk_widget_get_screen (widget);
681     /* Disconnect "size-changed" signal handler */
682     g_signal_handlers_disconnect_by_func (screen, G_CALLBACK (screen_size_changed), widget);
683
684     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unrealize (widget);
685 }
686
687 static void
688 hildon_app_menu_apply_style                     (GtkWidget *widget)
689 {
690     GdkScreen *screen;
691     gint filter_group_width;
692     guint horizontal_spacing, vertical_spacing, filter_vertical_spacing;
693     guint inner_border, external_border;
694     HildonAppMenuPrivate *priv;
695
696     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
697
698     gtk_widget_style_get (widget,
699                           "horizontal-spacing", &horizontal_spacing,
700                           "vertical-spacing", &vertical_spacing,
701                           "filter-group-width", &filter_group_width,
702                           "filter-vertical-spacing", &filter_vertical_spacing,
703                           "inner-border", &inner_border,
704                           "external-border", &external_border,
705                           NULL);
706
707     /* Set spacings */
708     gtk_table_set_row_spacings (priv->table, vertical_spacing);
709     gtk_table_set_col_spacings (priv->table, horizontal_spacing);
710     gtk_box_set_spacing (priv->vbox, filter_vertical_spacing);
711
712     /* Set inner border */
713     gtk_container_set_border_width (GTK_CONTAINER (widget), inner_border);
714
715     /* Set width of the group of filter buttons */
716     gtk_widget_set_size_request (GTK_WIDGET (priv->filters_hbox), filter_group_width, -1);
717
718     /* Compute width request */
719     screen = gtk_widget_get_screen (widget);
720     if (gdk_screen_get_width (screen) < gdk_screen_get_height (screen)) {
721         external_border = 0;
722     }
723     priv->width_request = gdk_screen_get_width (screen) - external_border * 2;
724
725     if (widget->window)
726       gdk_window_move_resize (widget->window,
727                               external_border, 0, 1, 1);
728
729     gtk_widget_queue_resize (widget);
730 }
731
732 static void
733 hildon_app_menu_style_set                       (GtkWidget *widget,
734                                                  GtkStyle  *previous_style)
735 {
736     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set)
737         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set (widget, previous_style);
738
739     hildon_app_menu_apply_style (widget);
740 }
741
742 static void
743 hildon_app_menu_repack_filters                  (HildonAppMenu *menu)
744 {
745     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
746     GList *iter;
747
748     for (iter = priv->filters; iter != NULL; iter = iter->next) {
749         GtkWidget *filter = GTK_WIDGET (iter->data);
750         GtkWidget *parent = gtk_widget_get_parent (filter);
751         if (parent) {
752             g_object_ref (filter);
753             gtk_container_remove (GTK_CONTAINER (parent), filter);
754         }
755     }
756
757     for (iter = priv->filters; iter != NULL; iter = iter->next) {
758         GtkWidget *filter = GTK_WIDGET (iter->data);
759         if (GTK_WIDGET_VISIBLE (filter)) {
760             gtk_box_pack_start (GTK_BOX (priv->filters_hbox), filter, TRUE, TRUE, 0);
761             g_object_unref (filter);
762             /* GtkButton must be realized for accelerators to work */
763             gtk_widget_realize (filter);
764         }
765     }
766 }
767
768 /*
769  * When items displayed in the menu change (e.g, a new item is added,
770  * an item is hidden or the list is reordered), the layout must be
771  * updated. To do this we repack all items starting from a given one.
772  */
773 static void
774 hildon_app_menu_repack_items                    (HildonAppMenu *menu,
775                                                  gint           start_from)
776 {
777     HildonAppMenuPrivate *priv;
778     gint row, col, nvisible, i;
779     GList *iter;
780
781     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
782
783     i = nvisible = 0;
784     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
785         /* Count number of visible items */
786         if (GTK_WIDGET_VISIBLE (iter->data))
787             nvisible++;
788         /* Remove buttons from their parent */
789         if (start_from != -1 && i >= start_from) {
790             GtkWidget *item = GTK_WIDGET (iter->data);
791             GtkWidget *parent = gtk_widget_get_parent (item);
792             if (parent) {
793                 g_object_ref (item);
794                 gtk_container_remove (GTK_CONTAINER (parent), item);
795             }
796         }
797         i++;
798     }
799
800     /* If items have been removed, recalculate the size of the menu */
801     if (start_from != -1)
802         gtk_window_resize (GTK_WINDOW (menu), 1, 1);
803
804     /* Set the final size now to avoid unnecessary resizes later */
805     if (nvisible > 0)
806         gtk_table_resize (priv->table, ((nvisible - 1) / priv->columns) + 1, priv->columns);
807
808     /* Add buttons */
809     row = col = 0;
810     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
811         GtkWidget *item = GTK_WIDGET (iter->data);
812         if (GTK_WIDGET_VISIBLE (item)) {
813             /* Don't add an item to the table if it's already there */
814             if (gtk_widget_get_parent (item) == NULL) {
815                 gtk_table_attach_defaults (priv->table, item, col, col + 1, row, row + 1);
816                 g_object_unref (item);
817                 /* GtkButton must be realized for accelerators to work */
818                 gtk_widget_realize (item);
819             }
820             if (++col == priv->columns) {
821                 col = 0;
822                 row++;
823             }
824         }
825     }
826
827     gtk_widget_queue_draw (GTK_WIDGET (menu));
828 }
829
830 /**
831  * hildon_app_menu_has_visible_children:
832  * @menu: a #HildonAppMenu
833  *
834  * Returns whether this menu has any visible items
835  * and/or filters. If this is %FALSE, the menu will
836  * not be popped up.
837  *
838  * Returns: whether there are visible items or filters
839  *
840  * Since: 2.2
841  **/
842 gboolean
843 hildon_app_menu_has_visible_children (HildonAppMenu *menu)
844 {
845    HildonAppMenuPrivate *priv;
846    GList *i;
847    gboolean show_menu = FALSE;
848
849     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
850
851     /* Don't show menu if it doesn't contain visible items */
852     for (i = priv->buttons; i && !show_menu; i = i->next)
853         show_menu = GTK_WIDGET_VISIBLE (i->data);
854
855     for (i = priv->filters; i && !show_menu; i = i->next)
856             show_menu = GTK_WIDGET_VISIBLE (i->data);
857
858     return show_menu;
859 }
860
861 /**
862  * hildon_app_menu_popup:
863  * @menu: a #HildonAppMenu
864  * @parent_window: a #GtkWindow
865  *
866  * Displays a menu on top of a window and makes it available for
867  * selection.
868  *
869  * Since: 2.2
870  **/
871 void
872 hildon_app_menu_popup                           (HildonAppMenu *menu,
873                                                  GtkWindow     *parent_window)
874 {
875     g_return_if_fail (HILDON_IS_APP_MENU (menu));
876     g_return_if_fail (GTK_IS_WINDOW (parent_window));
877
878     if (hildon_app_menu_has_visible_children (menu)) {
879         hildon_app_menu_set_parent_window (menu, parent_window);
880         gtk_widget_show (GTK_WIDGET (menu));
881     }
882
883 }
884
885 /**
886  * hildon_app_menu_get_items:
887  * @menu: a #HildonAppMenu
888  *
889  * Returns a list of all items (regular items, not filters) contained
890  * in @menu.
891  *
892  * Returns: a newly-allocated list containing the items in @menu
893  *
894  * Since: 2.2
895  **/
896 GList *
897 hildon_app_menu_get_items                       (HildonAppMenu *menu)
898 {
899     HildonAppMenuPrivate *priv;
900
901     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
902
903     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
904
905     return g_list_copy (priv->buttons);
906 }
907
908 /**
909  * hildon_app_menu_get_filters:
910  * @menu: a #HildonAppMenu
911  *
912  * Returns a list of all filters contained in @menu.
913  *
914  * Returns: a newly-allocated list containing the filters in @menu
915  *
916  * Since: 2.2
917  **/
918 GList *
919 hildon_app_menu_get_filters                     (HildonAppMenu *menu)
920 {
921     HildonAppMenuPrivate *priv;
922
923     g_return_val_if_fail (HILDON_IS_APP_MENU (menu), NULL);
924
925     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
926
927     return g_list_copy (priv->filters);
928 }
929
930 static void
931 hildon_app_menu_init                            (HildonAppMenu *menu)
932 {
933     GtkWidget *alignment;
934     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
935
936     /* Initialize private variables */
937     priv->parent_window = NULL;
938     priv->transfer_window = NULL;
939     priv->pressed_outside = FALSE;
940     priv->inhibit_repack = FALSE;
941     priv->last_pressed_button = NULL;
942     priv->buttons = NULL;
943     priv->filters = NULL;
944     priv->columns = 2;
945     priv->width_request = -1;
946     priv->find_intruder_idle_id = 0;
947     priv->hide_idle_id = 0;
948
949     /* Create boxes and tables */
950     priv->filters_hbox = GTK_BOX (gtk_hbox_new (TRUE, 0));
951     priv->vbox = GTK_BOX (gtk_vbox_new (FALSE, 0));
952     priv->table = GTK_TABLE (gtk_table_new (1, priv->columns, TRUE));
953
954     /* Align the filters to the center */
955     alignment = gtk_alignment_new (0.5, 0.5, 0, 0);
956     gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (priv->filters_hbox));
957
958     /* Pack everything */
959     gtk_container_add (GTK_CONTAINER (menu), GTK_WIDGET (priv->vbox));
960     gtk_box_pack_start (priv->vbox, alignment, TRUE, TRUE, 0);
961     gtk_box_pack_start (priv->vbox, GTK_WIDGET (priv->table), TRUE, TRUE, 0);
962
963     /* This should be treated like a normal, ref-counted widget */
964     g_object_force_floating (G_OBJECT (menu));
965     GTK_WINDOW (menu)->has_user_ref_count = FALSE;
966
967     gtk_widget_show_all (GTK_WIDGET (priv->vbox));
968 }
969
970 static void
971 hildon_app_menu_finalize                        (GObject *object)
972 {
973     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(object);
974
975     if (priv->find_intruder_idle_id) {
976         g_source_remove (priv->find_intruder_idle_id);
977         priv->find_intruder_idle_id = 0;
978     }
979
980     if (priv->hide_idle_id) {
981         g_source_remove (priv->hide_idle_id);
982         priv->hide_idle_id = 0;
983     }
984
985     if (priv->parent_window) {
986         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_topmost_notify, object);
987         g_signal_handlers_disconnect_by_func (priv->parent_window, parent_window_unmapped, object);
988     }
989
990     if (priv->transfer_window)
991         gdk_window_destroy (priv->transfer_window);
992
993     g_list_foreach (priv->buttons, (GFunc) g_object_unref, NULL);
994     g_list_foreach (priv->filters, (GFunc) g_object_unref, NULL);
995
996     g_list_free (priv->buttons);
997     g_list_free (priv->filters);
998
999     g_signal_handlers_destroy (object);
1000     G_OBJECT_CLASS (hildon_app_menu_parent_class)->finalize (object);
1001 }
1002
1003 static void
1004 hildon_app_menu_class_init                      (HildonAppMenuClass *klass)
1005 {
1006     GObjectClass *gobject_class = (GObjectClass *)klass;
1007     GtkWidgetClass *widget_class = (GtkWidgetClass *)klass;
1008
1009     gobject_class->finalize = hildon_app_menu_finalize;
1010     widget_class->show_all = hildon_app_menu_show_all;
1011     widget_class->hide_all = hildon_app_menu_hide_all;
1012     widget_class->map = hildon_app_menu_map;
1013     widget_class->realize = hildon_app_menu_realize;
1014     widget_class->unrealize = hildon_app_menu_unrealize;
1015     widget_class->grab_notify = hildon_app_menu_grab_notify;
1016     widget_class->key_press_event = hildon_app_menu_key_press;
1017     widget_class->style_set = hildon_app_menu_style_set;
1018     widget_class->delete_event = hildon_app_menu_delete_event_handler;
1019     widget_class->size_request = hildon_app_menu_size_request;
1020
1021     g_type_class_add_private (klass, sizeof (HildonAppMenuPrivate));
1022
1023     gtk_widget_class_install_style_property (
1024         widget_class,
1025         g_param_spec_uint (
1026             "horizontal-spacing",
1027             "Horizontal spacing on menu items",
1028             "Horizontal spacing between each menu item. Does not apply to filter buttons.",
1029             0, G_MAXUINT, 16,
1030             G_PARAM_READABLE));
1031
1032     gtk_widget_class_install_style_property (
1033         widget_class,
1034         g_param_spec_uint (
1035             "vertical-spacing",
1036             "Vertical spacing on menu items",
1037             "Vertical spacing between each menu item. Does not apply to filter buttons.",
1038             0, G_MAXUINT, 16,
1039             G_PARAM_READABLE));
1040
1041     gtk_widget_class_install_style_property (
1042         widget_class,
1043         g_param_spec_int (
1044             "filter-group-width",
1045             "Width of the group of filter buttons",
1046             "Total width of the group of filter buttons, "
1047             "or -1 to use the natural size request.",
1048             -1, G_MAXINT, 444,
1049             G_PARAM_READABLE));
1050
1051     gtk_widget_class_install_style_property (
1052         widget_class,
1053         g_param_spec_uint (
1054             "filter-vertical-spacing",
1055             "Vertical spacing between filters and menu items",
1056             "Vertical spacing between filters and menu items",
1057             0, G_MAXUINT, 8,
1058             G_PARAM_READABLE));
1059
1060     gtk_widget_class_install_style_property (
1061         widget_class,
1062         g_param_spec_uint (
1063             "inner-border",
1064             "Border between menu edges and buttons",
1065             "Border between menu edges and buttons",
1066             0, G_MAXUINT, 16,
1067             G_PARAM_READABLE));
1068
1069     gtk_widget_class_install_style_property (
1070         widget_class,
1071         g_param_spec_uint (
1072             "external-border",
1073             "Border between menu and screen edges (in horizontal mode)",
1074             "Border between the right and left edges of the menu and "
1075             "the screen edges (in horizontal mode)",
1076             0, G_MAXUINT, 50,
1077             G_PARAM_READABLE));
1078
1079
1080     /**
1081      * HildonAppMenu::changed:
1082      * @widget: the widget that received the signal
1083      *
1084      * The HildonAppMenu::changed signal is emitted whenever an
1085      * item or filter is added or removed from the menu.
1086      *
1087      * Since: 2.2
1088      */
1089     app_menu_signals[CHANGED] =
1090         g_signal_new ("changed",
1091                       G_TYPE_FROM_CLASS (klass),
1092                       G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
1093                       0,
1094                       NULL, NULL,
1095                       g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0, NULL);
1096 }