2008-08-26 Alberto Garcia <agarcia@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 static GdkWindow *
93 grab_transfer_window_get                        (GtkWidget *widget);
94
95 G_DEFINE_TYPE (HildonAppMenu, hildon_app_menu, GTK_TYPE_WINDOW);
96
97 /**
98  * hildon_app_menu_new:
99  *
100  * Creates a new #HildonAppMenu.
101  *
102  * Return value: A #HildonAppMenu.
103  **/
104 GtkWidget *
105 hildon_app_menu_new                             (void)
106 {
107     GtkWidget *menu = g_object_new (HILDON_TYPE_APP_MENU, NULL);
108     return menu;
109 }
110
111 /**
112  * hildon_app_menu_append:
113  * @menu : A #HildonAppMenu
114  * @item : A #GtkButton to add to the #HildonAppMenu
115  *
116  * Adds the @item to @menu.
117  */
118 void
119 hildon_app_menu_append                          (HildonAppMenu *menu,
120                                                  GtkButton *item)
121 {
122     HildonAppMenuPrivate *priv;
123     int row, col;
124
125     g_return_if_fail (HILDON_IS_APP_MENU (menu));
126     g_return_if_fail (GTK_IS_BUTTON (item));
127
128     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
129
130     /* Calculate the row and column number */
131     col = priv->nitems % 2;
132     row = (priv->nitems - col) / 2;
133     priv->nitems++;
134
135     /* GtkTable already calls gtk_table_resize() if necessary */
136     gtk_table_attach_defaults (priv->table, GTK_WIDGET (item), col, col + 1, row, row + 1);
137
138     /* Close the menu when the button is clicked */
139     g_signal_connect_swapped (item, "clicked", G_CALLBACK (gtk_widget_hide), menu);
140
141     gtk_widget_show (GTK_WIDGET (item));
142 }
143
144 /**
145  * hildon_app_menu_add_filter
146  * @menu : A #HildonAppMenu
147  * @filter : A #GtkButton to add to the #HildonAppMenu.
148  *
149  * Adds the @filter to @menu.
150  */
151 void
152 hildon_app_menu_add_filter                      (HildonAppMenu *menu,
153                                                  GtkButton *filter)
154 {
155     HildonAppMenuPrivate *priv;
156
157     g_return_if_fail (HILDON_IS_APP_MENU (menu));
158     g_return_if_fail (GTK_IS_BUTTON (filter));
159
160     priv = HILDON_APP_MENU_GET_PRIVATE(menu);
161
162     /* Pack the filter in the group and set its size */
163     gtk_box_pack_start (GTK_BOX (priv->filters_hbox), GTK_WIDGET (filter), TRUE, TRUE, 0);
164
165     /* Close the menu when the button is clicked */
166     g_signal_connect_swapped (filter, "clicked", G_CALLBACK (gtk_widget_hide), menu);
167
168     gtk_widget_show (GTK_WIDGET (filter));
169 }
170
171 static void
172 hildon_app_menu_map                             (GtkWidget *widget)
173 {
174     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
175
176     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->map (widget);
177
178     /* Grab pointer and keyboard */
179     if (priv->transfer_window == NULL) {
180         gboolean has_grab = FALSE;
181
182         priv->transfer_window = grab_transfer_window_get (widget);
183
184         if (gdk_pointer_grab (priv->transfer_window, TRUE,
185                               GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
186                               GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
187                               GDK_POINTER_MOTION_MASK, NULL, NULL,
188                               GDK_CURRENT_TIME) == GDK_GRAB_SUCCESS) {
189             if (gdk_keyboard_grab (priv->transfer_window, TRUE,
190                                    GDK_CURRENT_TIME) == GDK_GRAB_SUCCESS) {
191                 has_grab = TRUE;
192             } else {
193                 gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
194                                             GDK_CURRENT_TIME);
195             }
196         }
197
198         if (has_grab) {
199             gtk_grab_add (widget);
200         } else {
201             gdk_window_destroy (priv->transfer_window);
202             priv->transfer_window = NULL;
203         }
204     }
205 }
206
207 static void
208 hildon_app_menu_unmap                           (GtkWidget *widget)
209 {
210     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
211
212     /* Remove the grab */
213     if (priv->transfer_window != NULL) {
214         gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
215                                     GDK_CURRENT_TIME);
216         gtk_grab_remove (widget);
217
218         gdk_window_destroy (priv->transfer_window);
219         priv->transfer_window = NULL;
220     }
221
222     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->unmap (widget);
223 }
224
225 static gboolean
226 hildon_app_menu_button_press                    (GtkWidget *widget,
227                                                  GdkEventButton *event)
228 {
229     int x, y;
230     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
231
232     gdk_window_get_position (widget->window, &x, &y);
233
234     /* Whether the button has been pressed outside the widget */
235     priv->pressed_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
236                              event->y_root < y || event->y_root > y + widget->allocation.height);
237
238     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event) {
239         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_press_event (widget, event);
240     } else {
241         return FALSE;
242     }
243 }
244
245 static gboolean
246 hildon_app_menu_button_release                  (GtkWidget *widget,
247                                                  GdkEventButton *event)
248 {
249     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(widget);
250
251     if (priv->pressed_outside) {
252         int x, y;
253         gboolean released_outside;
254
255         gdk_window_get_position (widget->window, &x, &y);
256
257         /* Whether the button has been released outside the widget */
258         released_outside = (event->x_root < x || event->x_root > x + widget->allocation.width ||
259                             event->y_root < y || event->y_root > y + widget->allocation.height);
260
261         if (released_outside) {
262             gtk_widget_hide (widget);
263         }
264
265         priv->pressed_outside = FALSE; /* Always reset pressed_outside to FALSE */
266     }
267
268     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event) {
269         return GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->button_release_event (widget, event);
270     } else {
271         return FALSE;
272     }
273 }
274
275 /* Grab transfer window (based on the one from GtkMenu) */
276 static GdkWindow *
277 grab_transfer_window_get                        (GtkWidget *widget)
278 {
279     GdkWindow *window;
280     GdkWindowAttr attributes;
281     gint attributes_mask;
282
283     attributes.x = 0;
284     attributes.y = 0;
285     attributes.width = 10;
286     attributes.height = 10;
287     attributes.window_type = GDK_WINDOW_TEMP;
288     attributes.wclass = GDK_INPUT_ONLY;
289     attributes.override_redirect = TRUE;
290     attributes.event_mask = 0;
291
292     attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_NOREDIR;
293
294     window = gdk_window_new (gtk_widget_get_root_window (widget),
295                                  &attributes, attributes_mask);
296     gdk_window_set_user_data (window, widget);
297
298     gdk_window_show (window);
299
300     return window;
301 }
302
303 static void
304 hildon_app_menu_realize                         (GtkWidget *widget)
305 {
306     GdkDisplay *display;
307     Atom atom;
308     const gchar *notification_type = "_HILDON_WM_WINDOW_TYPE_APP_MENU";
309
310     GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->realize (widget);
311
312     gdk_window_set_decorations (widget->window, GDK_DECOR_BORDER);
313
314     display = gdk_drawable_get_display (widget->window);
315     atom = gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_WINDOW_TYPE");
316     XChangeProperty (GDK_WINDOW_XDISPLAY (widget->window), GDK_WINDOW_XID (widget->window),
317                      atom, XA_STRING, 8, PropModeReplace, (guchar *) notification_type,
318                      strlen (notification_type));
319 }
320
321 static void
322 hildon_app_menu_style_set                       (GtkWidget *widget,
323                                                  GtkStyle  *previous_style)
324 {
325     GdkScreen *screen;
326     gint width;
327     guint horizontal_spacing, vertical_spacing, inner_border, external_border;
328     HildonAppMenuPrivate *priv;
329
330     if (GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set)
331         GTK_WIDGET_CLASS (hildon_app_menu_parent_class)->style_set (widget, previous_style);
332
333     priv = HILDON_APP_MENU_GET_PRIVATE (widget);
334
335     gtk_widget_style_get (widget,
336                           "horizontal-spacing", &horizontal_spacing,
337                           "vertical-spacing", &vertical_spacing,
338                           "inner-border", &inner_border,
339                           "external-border", &external_border,
340                           NULL);
341
342     /* Set spacings */
343     gtk_table_set_row_spacings (priv->table, vertical_spacing);
344     gtk_table_set_col_spacings (priv->table, horizontal_spacing);
345     gtk_box_set_spacing (priv->vbox, vertical_spacing);
346
347     /* Set inner border */
348     gtk_container_set_border_width (GTK_CONTAINER (widget), inner_border);
349
350     /* Set default size */
351     screen = gtk_widget_get_screen (widget);
352     width = gdk_screen_get_width (screen) - external_border * 2;
353     gtk_window_set_default_size (GTK_WINDOW (widget), width, -1);
354 }
355
356 static void
357 hildon_app_menu_init                            (HildonAppMenu *menu)
358 {
359     GtkWidget *alignment;
360     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(menu);
361
362     /* Initialize private variables */
363     priv->filters_hbox = GTK_BOX (gtk_hbox_new (TRUE, 0));
364     priv->vbox = GTK_BOX (gtk_vbox_new (FALSE, 0));
365     priv->table = GTK_TABLE (gtk_table_new (1, 2, TRUE));
366     priv->nitems = 0;
367     priv->transfer_window = NULL;
368     priv->pressed_outside = FALSE;
369
370     /* Align the filters to the center */
371     alignment = gtk_alignment_new (0.5, 0.5, 0, 0);
372     gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (priv->filters_hbox));
373
374     /* Pack everything */
375     gtk_container_add (GTK_CONTAINER (menu), GTK_WIDGET (priv->vbox));
376     gtk_box_pack_start (priv->vbox, alignment, TRUE, TRUE, 0);
377     gtk_box_pack_start (priv->vbox, GTK_WIDGET (priv->table), TRUE, TRUE, 0);
378
379     gtk_window_set_modal (GTK_WINDOW (menu), TRUE);
380
381     gtk_widget_show_all (GTK_WIDGET (priv->vbox));
382 }
383
384 static void
385 hildon_app_menu_finalize                        (GObject *object)
386 {
387     HildonAppMenuPrivate *priv = HILDON_APP_MENU_GET_PRIVATE(object);
388
389     if (priv->transfer_window)
390         gdk_window_destroy (priv->transfer_window);
391
392     g_signal_handlers_destroy (object);
393     G_OBJECT_CLASS (hildon_app_menu_parent_class)->finalize (object);
394 }
395
396 static void
397 hildon_app_menu_class_init                      (HildonAppMenuClass *klass)
398 {
399     GObjectClass *gobject_class = (GObjectClass *)klass;
400     GtkWidgetClass *widget_class = (GtkWidgetClass *)klass;
401
402     gobject_class->finalize = hildon_app_menu_finalize;
403     widget_class->map = hildon_app_menu_map;
404     widget_class->unmap = hildon_app_menu_unmap;
405     widget_class->realize = hildon_app_menu_realize;
406     widget_class->button_press_event = hildon_app_menu_button_press;
407     widget_class->button_release_event = hildon_app_menu_button_release;
408     widget_class->style_set = hildon_app_menu_style_set;
409
410     g_type_class_add_private (klass, sizeof (HildonAppMenuPrivate));
411
412     gtk_widget_class_install_style_property (
413         widget_class,
414         g_param_spec_uint (
415             "horizontal-spacing",
416             "Horizontal spacing on menu items",
417             "Horizontal spacing between each menu item. Does not apply to filter buttons.",
418             0, G_MAXUINT, 16,
419             G_PARAM_READABLE));
420
421     gtk_widget_class_install_style_property (
422         widget_class,
423         g_param_spec_uint (
424             "vertical-spacing",
425             "Vertical spacing on menu items",
426             "Vertical spacing between each menu item. Does not apply to filter buttons.",
427             0, G_MAXUINT, 16,
428             G_PARAM_READABLE));
429
430     gtk_widget_class_install_style_property (
431         widget_class,
432         g_param_spec_uint (
433             "inner-border",
434             "Border between menu edges and buttons",
435             "Border between menu edges and buttons",
436             0, G_MAXUINT, 16,
437             G_PARAM_READABLE));
438
439     gtk_widget_class_install_style_property (
440         widget_class,
441         g_param_spec_uint (
442             "external-border",
443             "Border between menu and screen edges",
444             "Border between the right and left edges of the menu and the screen edges",
445             0, G_MAXUINT, 40,
446             G_PARAM_READABLE));
447 }