2008-09-24 Claudio Saavedra <csaavedra@igalia.com>
[hildon] / src / 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: Karl Lattimer <karl.lattimer@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 two columns. Entries are added
28  * left to right and top to bottom.
29  *
30  * Besides that, the #HildonAppMenu can contain a group of filter buttons
31  * (#GtkToggleButton or #GtkRadioButton).
32  *
33  * To use a #HildonAppMenu, add it to a #HildonStackableWindow with
34  * with hildon_stackable_window_set_main_menu(). The menu will appear
35  * when the user presses the window title bar.
36  *
37  * Alternatively, you can show it by hand using gtk_widget_show().
38  *
39  * The menu will be automatically hidden when one of its buttons is
40  * clicked. Use g_signal_connect_after() when connecting callbacks to
41  * buttons to make sure that they're called after the menu
42  * disappears. Alternatively, you can add the button to the menu
43  * before connecting any callback.
44  *
45  * <example>
46  * <title>Creating a HildonAppMenu</title>
47  * <programlisting>
48  * HildonStackableWindow *win;
49  * HildonAppMenu *menu;
50  * GtkWidget *button;
51  * GtkWidget *filter;
52  * <!-- -->
53  * win = HILDON_STACKABLE_WINDOW (hildon_stackable_window_new ());
54  * menu = HILDON_APP_MENU (hildon_app_menu_new ());
55  * <!-- -->
56  * // Create a button and add it to the menu
57  * button = gtk_button_new_with_label ("Menu command one");
58  * g_signal_connect_after (button, "clicked", G_CALLBACK (button_one_clicked), userdata);
59  * hildon_app_menu_append (menu, GTK_BUTTON (button));
60  * <!-- -->
61  * // Another button
62  * button = gtk_button_new_with_label ("Menu command two");
63  * g_signal_connect_after (button, "clicked", G_CALLBACK (button_two_clicked), userdata);
64  * hildon_app_menu_append (menu, GTK_BUTTON (button));
65  * <!-- -->
66  * // Create a filter and add it to the menu
67  * filter = gtk_radio_button_new_with_label (NULL, "Filter one");
68  * gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (filter), FALSE);
69  * g_signal_connect_after (filter, "clicked", G_CALLBACK (filter_one_clicked), userdata);
70  * hildon_app_menu_add_filter (menu, GTK_BUTTON (filter));
71  * <!-- -->
72  * // Add a new filter
73  * filter = gtk_radio_button_new_with_label_from_widget (GTK_RADIO_BUTTON (filter), "Filter two");
74  * gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (filter), FALSE);
75  * g_signal_connect_after (filter, "clicked", G_CALLBACK (filter_two_clicked), userdata);
76  * hildon_app_menu_add_filter (menu, GTK_BUTTON (filter));
77  * <!-- -->
78  * // Add the menu to the window
79  * hildon_stackable_window_set_main_menu (win, menu);
80  * </programlisting>
81  * </example>
82  *
83  */
84
85 #include                                        <string.h>
86 #include                                        <X11/Xatom.h>
87 #include                                        <gdk/gdkx.h>
88
89 #include                                        "hildon-app-menu.h"
90 #include                                        "hildon-app-menu-private.h"
91
92 enum {
93     PROP_COLUMNS = 1
94 };
95
96 static GdkWindow *
97 grab_transfer_window_get                        (GtkWidget *widget);
98
99 static void
100 hildon_app_menu_construct_child                 (HildonAppMenu *menu);
101
102 static void
103 button_visibility_changed                       (GtkWidget     *item,
104                                                  GParamSpec    *arg1,
105                                                  HildonAppMenu *menu);
106
107 G_DEFINE_TYPE (HildonAppMenu, hildon_app_menu, GTK_TYPE_WINDOW);
108
109 /**
110  * hildon_app_menu_new:
111  *
112  * Creates a new #HildonAppMenu.
113  *
114  * Return value: A #HildonAppMenu.
115  **/
116 GtkWidget *
117 hildon_app_menu_new                             (void)
118 {
119     GtkWidget *menu = g_object_new (HILDON_TYPE_APP_MENU, NULL);
120     return menu;
121 }
122
123 /**
124  * hildon_app_menu_append:
125  * @menu : A #HildonAppMenu
126  * @item : A #GtkButton to add to the #HildonAppMenu
127  *
128  * Adds the @item to @menu.
129  */
130 void
131 hildon_app_menu_append                          (HildonAppMenu *menu,
132                                                  GtkButton *item)
133 {
134     HildonAppMenuPrivate *priv;
135
136     g_return_if_fail (HILDON_IS_APP_MENU (menu));
137     g_return_if_fail (GTK_IS_BUTTON (item));
138
139     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
140
141     /* Add the item to the menu */
142     gtk_widget_show (GTK_WIDGET (item));
143     priv->buttons = g_list_append (priv->buttons, item);
144     hildon_app_menu_construct_child (menu);
145
146     /* Close the menu when the button is clicked */
147     g_signal_connect_swapped (item, "clicked", G_CALLBACK (gtk_widget_hide), menu);
148     g_signal_connect (item, "notify::visible", G_CALLBACK (button_visibility_changed), menu);
149 }
150
151 /**
152  * hildon_app_menu_add_filter:
153  * @menu : A #HildonAppMenu
154  * @filter : A #GtkButton to add to the #HildonAppMenu.
155  *
156  * Adds the @filter to @menu.
157  */
158 void
159 hildon_app_menu_add_filter                      (HildonAppMenu *menu,
160                                                  GtkButton *filter)
161 {
162     HildonAppMenuPrivate *priv;
163
164     g_return_if_fail (HILDON_IS_APP_MENU (menu));
165     g_return_if_fail (GTK_IS_BUTTON (filter));
166
167     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
168
169     /* Add the filter to the menu */
170     gtk_widget_show (GTK_WIDGET (filter));
171     priv->filters = g_list_append (priv->filters, filter);
172     hildon_app_menu_construct_child (menu);
173
174     /* Close the menu when the button is clicked */
175     g_signal_connect_swapped (filter, "clicked", G_CALLBACK (gtk_widget_hide), menu);
176     g_signal_connect (filter, "notify::visible", G_CALLBACK (button_visibility_changed), menu);
177 }
178
179 static void
180 hildon_app_menu_set_columns                     (HildonAppMenu *menu,
181                                                  guint          columns)
182 {
183     HildonAppMenuPrivate *priv;
184
185     g_warning ("This property will be removed in the future. See documentation for details");
186
187     g_return_if_fail (HILDON_IS_APP_MENU (menu));
188     g_return_if_fail (columns > 0);
189
190     priv = HILDON_APP_MENU_GET_PRIVATE (menu);
191
192     if (columns != priv->columns) {
193         priv->columns = columns;
194         hildon_app_menu_construct_child (menu);
195     }
196 }
197
198 static void
199 hildon_app_menu_set_property                    (GObject      *object,
200                                                  guint         prop_id,
201                                                  const GValue *value,
202                                                  GParamSpec   *pspec)
203 {
204     HildonAppMenu *menu = HILDON_APP_MENU (object);
205
206     switch (prop_id)
207     {
208     case PROP_COLUMNS:
209         hildon_app_menu_set_columns (menu, g_value_get_uint (value));
210         break;
211     default:
212         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
213         break;
214     }
215 }
216
217 static void
218 button_visibility_changed                       (GtkWidget     *item,
219                                                  GParamSpec    *arg1,
220                                                  HildonAppMenu *menu)
221 {
222     hildon_app_menu_construct_child (menu);
223 }
224
225 static void
226 hildon_app_menu_map                             (GtkWidget *widget)
227 {
228     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
229
230     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->map (widget);
231
232     /* Grab pointer and keyboard */
233     if (priv->transfer_window == NULL) {
234         gboolean has_grab = FALSE;
235
236         priv->transfer_window = grab_transfer_window_get (widget);
237
238         if (gdk_pointer_grab (priv->transfer_window, TRUE,
239                               GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
240                               GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
241                               GDK_POINTER_MOTION_MASK, NULL, NULL,
242                               GDK_CURRENT_TIME) == GDK_GRAB_SUCCESS) {
243             if (gdk_keyboard_grab (priv->transfer_window, TRUE,
244                                    GDK_CURRENT_TIME) == GDK_GRAB_SUCCESS) {
245                 has_grab = TRUE;
246             } else {
247                 gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
248                                             GDK_CURRENT_TIME);
249             }
250         }
251
252         if (has_grab) {
253             gtk_grab_add (widget);
254         } else {
255             gdk_window_destroy (priv->transfer_window);
256             priv->transfer_window = NULL;
257         }
258     }
259 }
260
261 static void
262 hildon_app_menu_unmap                           (GtkWidget *widget)
263 {
264     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
265
266     /* Remove the grab */
267     if (priv->transfer_window != NULL) {
268         gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
269                                     GDK_CURRENT_TIME);
270         gtk_grab_remove (widget);
271
272         gdk_window_destroy (priv->transfer_window);
273         priv->transfer_window = NULL;
274     }
275
276     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unmap (widget);
277 }
278
279 static gboolean
280 hildon_app_menu_button_press                    (GtkWidget *widget,
281                                                  GdkEventButton *event)
282 {
283     int x, y;
284     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
285
286     gdk_window_get_position (widget->window, &x, &y);
287
288     /* Whether the button has been pressed outside the widget */
289     priv->pressed_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
290                              event->y_root < y || event->y_root > y + widget->allocation.height);
291
292     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event) {
293         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event (widget, event);
294     } else {
295         return FALSE;
296     }
297 }
298
299 static gboolean
300 hildon_app_menu_button_release                  (GtkWidget *widget,
301                                                  GdkEventButton *event)
302 {
303     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
304
305     if (priv->pressed_outside) {
306         int x, y;
307         gboolean released_outside;
308
309         gdk_window_get_position (widget->window, &x, &y);
310
311         /* Whether the button has been released outside the widget */
312         released_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
313                             event->y_root < y || event->y_root > y + widget->allocation.height);
314
315         if (released_outside) {
316             gtk_widget_hide (widget);
317         }
318
319         priv->pressed_outside = FALSE; /* Always reset pressed_outside to FALSE */
320     }
321
322     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event) {
323         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event (widget, event);
324     } else {
325         return FALSE;
326     }
327 }
328
329 /* Grab transfer window (based on the one from GtkMenu) */
330 static GdkWindow *
331 grab_transfer_window_get                        (GtkWidget *widget)
332 {
333     GdkWindow *window;
334     GdkWindowAttr attributes;
335     gint attributes_mask;
336
337     attributes.x = 0;
338     attributes.y = 0;
339     attributes.width = 10;
340     attributes.height = 10;
341     attributes.window_type = GDK_WINDOW_TEMP;
342     attributes.wclass = GDK_INPUT_ONLY;
343     attributes.override_redirect = TRUE;
344     attributes.event_mask = 0;
345
346     attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_NOREDIR;
347
348     window = gdk_window_new (gtk_widget_get_root_window (widget),
349                                  &attributes, attributes_mask);
350     gdk_window_set_user_data (window, widget);
351
352     gdk_window_show (window);
353
354     return window;
355 }
356
357 static void
358 hildon_app_menu_realize                         (GtkWidget *widget)
359 {
360     GdkDisplay *display;
361     Atom atom;
362     const gchar *notification_type = "_HILDON_WM_WINDOW_TYPE_APP_MENU";
363
364     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->realize (widget);
365
366     gdk_window_set_decorations (widget->window, GDK_DECOR_BORDER);
367
368     display = gdk_drawable_get_display (widget->window);
369     atom = gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_WINDOW_TYPE");
370     XChangeProperty (GDK_WINDOW_XDISPLAY (widget->window), GDK_WINDOW_XID (widget->window),
371                      atom, XA_STRING, 8, PropModeReplace, (guchar *) notification_type,
372                      strlen (notification_type));
373 }
374
375 static void
376 hildon_app_menu_apply_style                     (GtkWidget *widget)
377 {
378     GdkScreen *screen;
379     gint width;
380     guint horizontal_spacing, vertical_spacing, inner_border, external_border;
381     HildonAppMenuPrivate *priv;
382
383     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
384
385     gtk_widget_style_get (widget,
386                           "horizontal-spacing", &horizontal_spacing,
387                           "vertical-spacing", &vertical_spacing,
388                           "inner-border", &inner_border,
389                           "external-border", &external_border,
390                           NULL);
391
392     /* Set spacings */
393     gtk_table_set_row_spacings (priv->table, vertical_spacing);
394     gtk_table_set_col_spacings (priv->table, horizontal_spacing);
395     gtk_box_set_spacing (priv->vbox, vertical_spacing);
396
397     /* Set inner border */
398     gtk_container_set_border_width (GTK_CONTAINER (widget), inner_border);
399
400     /* Set default size */
401     screen = gtk_widget_get_screen (widget);
402     width = gdk_screen_get_width (screen) - external_border * 2;
403     gtk_window_set_default_size (GTK_WINDOW (widget), width, -1);
404 }
405
406 static void
407 hildon_app_menu_style_set                       (GtkWidget *widget,
408                                                  GtkStyle  *previous_style)
409 {
410     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set)
411         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set (widget, previous_style);
412
413     hildon_app_menu_apply_style (widget);
414 }
415
416 static void
417 hildon_app_menu_construct_child                 (HildonAppMenu *menu)
418 {
419     GtkWidget *alignment;
420     HildonAppMenuPrivate *priv;
421     gint col, row;
422     GList *iter;
423
424     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
425
426     /* Remove all buttons from their parents */
427     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
428         GtkWidget *item = GTK_WIDGET (iter->data);
429         GtkWidget *parent = gtk_widget_get_parent (item);
430         if (parent) {
431             g_object_ref (item);
432             gtk_container_remove (GTK_CONTAINER (parent), item);
433         }
434     }
435
436     for (iter = priv->filters; iter != NULL; iter = iter->next) {
437         GtkWidget *item = GTK_WIDGET (iter->data);
438         GtkWidget *parent = gtk_widget_get_parent (item);
439         if (parent) {
440             g_object_ref (item);
441             gtk_container_remove (GTK_CONTAINER (parent), item);
442         }
443     }
444
445     /* Create the contents of the menu again */
446     if (priv->vbox) {
447         gtk_widget_destroy (GTK_WIDGET (priv->vbox));
448     }
449
450     /* Resize the menu to its minimum size */
451     gtk_window_resize (GTK_WINDOW (menu), 1, 1);
452
453     /* Create boxes and tables */
454     priv->filters_hbox = GTK_BOX (gtk_hbox_new (TRUE, 0));
455     priv->vbox = GTK_BOX (gtk_vbox_new (FALSE, 0));
456     priv->table = GTK_TABLE (gtk_table_new (1, priv->columns, TRUE));
457
458     /* Align the filters to the center */
459     alignment = gtk_alignment_new (0.5, 0.5, 0, 0);
460     gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (priv->filters_hbox));
461
462     /* Pack everything */
463     gtk_container_add (GTK_CONTAINER (menu), GTK_WIDGET (priv->vbox));
464     gtk_box_pack_start (priv->vbox, alignment, TRUE, TRUE, 0);
465     gtk_box_pack_start (priv->vbox, GTK_WIDGET (priv->table), TRUE, TRUE, 0);
466
467     /* Apply style properties */
468     hildon_app_menu_apply_style (GTK_WIDGET (menu));
469
470     /* Add buttons */
471     col = row = 0;
472     for (iter = priv->buttons; iter != NULL; iter = iter->next) {
473         GtkWidget *item = GTK_WIDGET (iter->data);
474         if (GTK_WIDGET_VISIBLE (item)) {
475             gtk_table_attach_defaults (priv->table, item, col, col + 1, row, row + 1);
476             if (++col == priv->columns) {
477                 col = 0;
478                 row++;
479             }
480         }
481     }
482
483     for (iter = priv->filters; iter != NULL; iter = iter->next) {
484         GtkWidget *filter = GTK_WIDGET (iter->data);
485         if (GTK_WIDGET_VISIBLE (filter)) {
486             gtk_box_pack_start (GTK_BOX (priv->filters_hbox), filter, TRUE, TRUE, 0);
487         }
488     }
489
490     gtk_widget_show_all (GTK_WIDGET (priv->vbox));
491
492     if (GTK_WIDGET_VISIBLE (GTK_WIDGET (menu))) {
493         gtk_window_reshow_with_initial_size (GTK_WINDOW (menu));
494     }
495 }
496
497 static void
498 hildon_app_menu_init                            (HildonAppMenu *menu)
499 {
500     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
501
502     /* Initialize private variables */
503     priv->filters_hbox = NULL;
504     priv->vbox = NULL;
505     priv->table = NULL;
506     priv->transfer_window = NULL;
507     priv->pressed_outside = FALSE;
508     priv->buttons = NULL;
509     priv->filters = NULL;
510     priv->columns = 2;
511
512     hildon_app_menu_construct_child (menu);
513
514     gtk_window_set_modal (GTK_WINDOW (menu), TRUE);
515 }
516
517 static void
518 hildon_app_menu_finalize                        (GObject *object)
519 {
520     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(object);
521
522     if (priv->transfer_window)
523         gdk_window_destroy (priv->transfer_window);
524
525     g_list_free (priv->buttons);
526     g_list_free (priv->filters);
527
528     g_signal_handlers_destroy (object);
529     G_OBJECT_CLASS (hildon_app_menu_parent_class)->finalize (object);
530 }
531
532 static void
533 hildon_app_menu_class_init                      (HildonAppMenuClass *klass)
534 {
535     GObjectClass *gobject_class = (GObjectClass *)klass;
536     GtkWidgetClass *widget_class = (GtkWidgetClass *)klass;
537
538     gobject_class->finalize = hildon_app_menu_finalize;
539     gobject_class->set_property = hildon_app_menu_set_property;
540     widget_class->map = hildon_app_menu_map;
541     widget_class->unmap = hildon_app_menu_unmap;
542     widget_class->realize = hildon_app_menu_realize;
543     widget_class->button_press_event = hildon_app_menu_button_press;
544     widget_class->button_release_event = hildon_app_menu_button_release;
545     widget_class->style_set = hildon_app_menu_style_set;
546
547     g_type_class_add_private (klass, sizeof (HildonAppMenuPrivate));
548
549     g_object_class_install_property (
550         gobject_class,
551         PROP_COLUMNS,
552         g_param_spec_uint (
553             "columns",
554             "Columns",
555             "Number of columns used to display menu items. "
556             "IMPORTANT: this is a temporary property. Don't use unless really needed. "
557             "The number of columns will be managed automatically in the future, "
558             "and this property will be removed.",
559             1, G_MAXUINT, 2,
560             G_PARAM_WRITABLE));
561
562     gtk_widget_class_install_style_property (
563         widget_class,
564         g_param_spec_uint (
565             "horizontal-spacing",
566             "Horizontal spacing on menu items",
567             "Horizontal spacing between each menu item. Does not apply to filter buttons.",
568             0, G_MAXUINT, 16,
569             G_PARAM_READABLE));
570
571     gtk_widget_class_install_style_property (
572         widget_class,
573         g_param_spec_uint (
574             "vertical-spacing",
575             "Vertical spacing on menu items",
576             "Vertical spacing between each menu item. Does not apply to filter buttons.",
577             0, G_MAXUINT, 16,
578             G_PARAM_READABLE));
579
580     gtk_widget_class_install_style_property (
581         widget_class,
582         g_param_spec_uint (
583             "inner-border",
584             "Border between menu edges and buttons",
585             "Border between menu edges and buttons",
586             0, G_MAXUINT, 16,
587             G_PARAM_READABLE));
588
589     gtk_widget_class_install_style_property (
590         widget_class,
591         g_param_spec_uint (
592             "external-border",
593             "Border between menu and screen edges",
594             "Border between the right and left edges of the menu and the screen edges",
595             0, G_MAXUINT, 40,
596             G_PARAM_READABLE));
597 }