]> Pileus Git - ~andy/gtk/blob - gtk/gtkmenubutton.c
e5f47a64a5a50421099e3f7c4854227bb4f920bf
[~andy/gtk] / gtk / gtkmenubutton.c
1 /* GTK - The GIMP Toolkit
2  *
3  * Copyright (C) 2003 Ricardo Fernandez Pascual
4  * Copyright (C) 2004 Paolo Borelli
5  * Copyright (C) 2012 Bastien Nocera
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library 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 GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 #include "config.h"
22
23 #include "gtkmenubutton.h"
24 #include "gtkmenubuttonprivate.h"
25 #include "gtkarrow.h"
26
27 #include "gtkprivate.h"
28 #include "gtkintl.h"
29
30 struct _GtkMenuButtonPrivate
31 {
32   GtkWidget *menu;
33   GMenuModel *model;
34
35   GtkMenuButtonShowMenuCallback func;
36   gpointer user_data;
37
38   GtkArrowType arrow_type;
39   GtkWidget *parent;
40 };
41
42 enum
43 {
44   PROP_0,
45   PROP_MENU,
46   PROP_MODEL,
47   PROP_PARENT,
48   PROP_DIRECTION
49 };
50
51 G_DEFINE_TYPE(GtkMenuButton, gtk_menu_button, GTK_TYPE_TOGGLE_BUTTON)
52
53 static void gtk_menu_button_finalize (GObject *object);
54
55 static void
56 gtk_menu_button_set_property (GObject      *object,
57                                 guint         property_id,
58                                 const GValue *value,
59                                 GParamSpec   *pspec)
60 {
61   switch (property_id)
62     {
63       case PROP_MENU:
64         gtk_menu_button_set_menu (GTK_MENU_BUTTON (object), g_value_get_object (value));
65         break;
66       case PROP_MODEL:
67         gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (object), g_value_get_object (value));
68         break;
69       case PROP_PARENT:
70         gtk_menu_button_set_parent (GTK_MENU_BUTTON (object), g_value_get_object (value));
71         break;
72       case PROP_DIRECTION:
73         gtk_menu_button_set_direction (GTK_MENU_BUTTON (object), g_value_get_enum (value));
74         break;
75       default:
76         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
77     }
78 }
79
80 static void
81 gtk_menu_button_get_property (GObject    *object,
82                                 guint       property_id,
83                                 GValue     *value,
84                                 GParamSpec *pspec)
85 {
86   GtkMenuButtonPrivate *priv = GTK_MENU_BUTTON (object)->priv;
87
88   switch (property_id)
89     {
90       case PROP_MENU:
91         g_value_set_object (value, G_OBJECT (priv->menu));
92         break;
93       case PROP_MODEL:
94         g_value_set_object (value, G_OBJECT (priv->model));
95         break;
96       case PROP_PARENT:
97         g_value_set_object (value, G_OBJECT (priv->parent));
98         break;
99       case PROP_DIRECTION:
100         g_value_set_enum (value, priv->arrow_type);
101         break;
102       default:
103         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
104     }
105 }
106
107 static void
108 gtk_menu_button_state_changed (GtkWidget    *widget,
109                                GtkStateType  previous_state)
110 {
111   GtkMenuButton *button = GTK_MENU_BUTTON (widget);
112   GtkMenuButtonPrivate *priv = button->priv;
113
114   if (!gtk_widget_is_sensitive (widget) && priv->menu)
115     {
116       gtk_menu_shell_deactivate (GTK_MENU_SHELL (priv->menu));
117     }
118 }
119
120 static void
121 gtk_menu_button_class_init (GtkMenuButtonClass *klass)
122 {
123   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
124   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
125
126   g_type_class_add_private (klass, sizeof (GtkMenuButtonPrivate));
127
128   gobject_class->set_property = gtk_menu_button_set_property;
129   gobject_class->get_property = gtk_menu_button_get_property;
130   gobject_class->finalize = gtk_menu_button_finalize;
131
132   widget_class->state_changed = gtk_menu_button_state_changed;
133
134   g_object_class_install_property (gobject_class,
135                                    PROP_MENU,
136                                    g_param_spec_object ("menu",
137                                                         P_("menu"),
138                                                         P_("The dropdown menu."),
139                                                         GTK_TYPE_MENU,
140                                                         G_PARAM_READWRITE));
141   g_object_class_install_property (gobject_class,
142                                    PROP_MODEL,
143                                    g_param_spec_object ("model",
144                                                         P_("model"),
145                                                         P_("The dropdown menu's model."),
146                                                         G_TYPE_MENU_MODEL,
147                                                         G_PARAM_READWRITE));
148   g_object_class_install_property (gobject_class,
149                                    PROP_PARENT,
150                                    g_param_spec_object ("parent",
151                                                         P_("parent"),
152                                                         P_("The parent widget which the menu should align with."),
153                                                         GTK_TYPE_CONTAINER,
154                                                         G_PARAM_READWRITE));
155   g_object_class_install_property (gobject_class,
156                                    PROP_DIRECTION,
157                                    g_param_spec_enum ("direction",
158                                                       P_("direction"),
159                                                       P_("The direction the arrow should point."),
160                                                       GTK_TYPE_ARROW_TYPE,
161                                                       GTK_ARROW_DOWN,
162                                                       G_PARAM_READWRITE));
163 }
164
165 static void
166 menu_position_up_func (GtkMenu         *menu,
167                        gint            *x,
168                        gint            *y,
169                        gboolean        *push_in,
170                        GtkMenuButton   *menu_button)
171 {
172   GtkWidget *widget = GTK_WIDGET (menu_button);
173   GtkRequisition menu_req;
174   GtkTextDirection direction;
175   GtkAllocation toggle_allocation;
176   GdkRectangle monitor;
177   gint monitor_num;
178   GdkScreen *screen;
179   GdkWindow *window;
180
181   gtk_widget_get_preferred_size (GTK_WIDGET (menu),
182                                  &menu_req, NULL);
183
184   direction = gtk_widget_get_direction (widget);
185   window = gtk_widget_get_window (widget);
186
187   screen = gtk_widget_get_screen (GTK_WIDGET (menu));
188   monitor_num = gdk_screen_get_monitor_at_window (screen, window);
189   if (monitor_num < 0)
190     monitor_num = 0;
191   gdk_screen_get_monitor_workarea (screen, monitor_num, &monitor);
192
193   gdk_window_get_origin (gtk_button_get_event_window (GTK_BUTTON (menu_button)), x, y);
194
195   gtk_widget_get_allocation (widget, &toggle_allocation);
196
197   if (direction == GTK_TEXT_DIR_LTR)
198     *x += MAX (toggle_allocation.width - menu_req.width, 0);
199   else if (menu_req.width > toggle_allocation.width)
200     *x -= menu_req.width - toggle_allocation.width;
201
202   if (*y - menu_req.height > monitor.y)
203     *y -= menu_req.height + toggle_allocation.y;
204
205   *push_in = FALSE;
206 }
207
208 static void
209 menu_position_side_func (GtkMenu         *menu,
210                          int             *x,
211                          int             *y,
212                          gboolean        *push_in,
213                          GtkMenuButton   *menu_button)
214 {
215   GtkMenuButtonPrivate *priv = menu_button->priv;
216   GtkAllocation toggle_allocation;
217   GtkWidget *widget = GTK_WIDGET (menu_button);
218   GtkRequisition menu_req;
219   GdkRectangle monitor;
220   gint monitor_num;
221   GdkScreen *screen;
222   GdkWindow *window;
223
224   gtk_widget_get_preferred_size (GTK_WIDGET (priv->menu),
225                                  &menu_req, NULL);
226
227   window = gtk_widget_get_window (widget);
228
229   screen = gtk_widget_get_screen (GTK_WIDGET (menu));
230   monitor_num = gdk_screen_get_monitor_at_window (screen, window);
231   if (monitor_num < 0)
232     monitor_num = 0;
233   gdk_screen_get_monitor_workarea (screen, monitor_num, &monitor);
234
235   gdk_window_get_origin (gtk_button_get_event_window (GTK_BUTTON (menu_button)), x, y);
236
237   gtk_widget_get_allocation (widget, &toggle_allocation);
238
239   if (priv->arrow_type == GTK_ARROW_RIGHT)
240     *x += toggle_allocation.width;
241   else
242     *x -= menu_req.width;
243
244   if (*y + menu_req.height > monitor.y + monitor.height &&
245       *y + toggle_allocation.height - monitor.y > monitor.y + monitor.height - *y)
246     *y += toggle_allocation.height - menu_req.height;
247
248   *push_in = FALSE;
249 }
250
251 static void
252 menu_position_down_func (GtkMenu         *menu,
253                          int             *x,
254                          int             *y,
255                          gboolean        *push_in,
256                          GtkMenuButton   *menu_button)
257 {
258   GtkMenuButtonPrivate *priv = menu_button->priv;
259   GtkWidget *widget = GTK_WIDGET (menu_button);
260   GtkRequisition menu_req;
261   GtkTextDirection direction;
262   GdkRectangle monitor;
263   gint monitor_num;
264   GdkScreen *screen;
265   GdkWindow *window;
266   GtkAllocation allocation, arrow_allocation;
267
268   gtk_widget_get_preferred_size (GTK_WIDGET (priv->menu),
269                                  &menu_req, NULL);
270
271   direction = gtk_widget_get_direction (widget);
272   window = gtk_widget_get_window (priv->parent ? priv->parent : widget);
273
274   screen = gtk_widget_get_screen (GTK_WIDGET (menu));
275   monitor_num = gdk_screen_get_monitor_at_window (screen, window);
276   if (monitor_num < 0)
277     monitor_num = 0;
278   gdk_screen_get_monitor_workarea (screen, monitor_num, &monitor);
279
280   gtk_widget_get_allocation (priv->parent ? priv->parent : widget, &allocation);
281   gtk_widget_get_allocation (widget, &arrow_allocation);
282
283   gdk_window_get_origin (window, x, y);
284   *x += allocation.x;
285   *y += allocation.y;
286
287   if (direction == GTK_TEXT_DIR_LTR)
288     *x += MAX (allocation.width - menu_req.width, 0);
289   else if (menu_req.width > allocation.width)
290     *x -= menu_req.width - allocation.width;
291
292   if ((*y + arrow_allocation.height + menu_req.height) <= monitor.y + monitor.height)
293     *y += arrow_allocation.height;
294   else if ((*y - menu_req.height) >= monitor.y)
295     *y -= menu_req.height;
296   else if (monitor.y + monitor.height - (*y + arrow_allocation.height) > *y)
297     *y += arrow_allocation.height;
298   else
299     *y -= menu_req.height;
300
301   *push_in = FALSE;
302 }
303
304 static void
305 popup_menu (GtkMenuButton  *menu_button,
306             GdkEventButton *event)
307 {
308   GtkMenuButtonPrivate *priv = menu_button->priv;
309   GtkMenuPositionFunc func;
310
311   if (priv->func)
312     priv->func (priv->user_data);
313
314   if (!priv->menu)
315     return;
316
317   switch (priv->arrow_type)
318     {
319       case GTK_ARROW_UP:
320         func = (GtkMenuPositionFunc) menu_position_up_func;
321         break;
322       case GTK_ARROW_LEFT:
323       case GTK_ARROW_RIGHT:
324         func = (GtkMenuPositionFunc) menu_position_side_func;
325         break;
326       default:
327         func = (GtkMenuPositionFunc) menu_position_down_func;
328         break;
329   }
330
331   gtk_menu_popup (GTK_MENU (priv->menu), NULL, NULL,
332                   func,
333                   GTK_WIDGET (menu_button),
334                   event ? event->button : 0,
335                   event ? event->time : gtk_get_current_event_time ());
336 }
337
338 static void
339 menu_button_toggled_cb (GtkMenuButton *menu_button)
340 {
341   GtkMenuButtonPrivate *priv = menu_button->priv;
342
343   if (!priv->menu)
344     return;
345
346   if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (menu_button)) &&
347       !gtk_widget_get_visible (GTK_WIDGET (priv->menu)))
348     {
349       /* we get here only when the menu is activated by a key
350        * press, so that we can select the first menu item */
351       popup_menu (menu_button, NULL);
352       gtk_menu_shell_select_first (GTK_MENU_SHELL (priv->menu), FALSE);
353     }
354 }
355
356 static gboolean
357 menu_button_button_press_event_cb (GtkWidget         *widget,
358                                     GdkEventButton    *event,
359                                     GtkMenuButton *button)
360 {
361   if (event->button == GDK_BUTTON_PRIMARY)
362     {
363       popup_menu (button, event);
364       gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE);
365
366       return TRUE;
367     }
368   else
369     {
370       return FALSE;
371     }
372 }
373
374 static void
375 gtk_menu_button_init (GtkMenuButton *menu_button)
376 {
377   GtkMenuButtonPrivate *priv;
378   GtkWidget *arrow;
379
380   priv = G_TYPE_INSTANCE_GET_PRIVATE (menu_button, GTK_TYPE_MENU_BUTTON, GtkMenuButtonPrivate);
381   menu_button->priv = priv;
382   priv->arrow_type = GTK_ARROW_DOWN;
383
384   arrow = gtk_arrow_new (priv->arrow_type, GTK_SHADOW_NONE);
385   gtk_container_add (GTK_CONTAINER (menu_button), arrow);
386   gtk_widget_show (arrow);
387
388   gtk_widget_set_sensitive (GTK_WIDGET (menu_button), FALSE);
389
390   g_signal_connect (menu_button, "toggled",
391                     G_CALLBACK (menu_button_toggled_cb), menu_button);
392   g_signal_connect (menu_button, "button-press-event",
393                     G_CALLBACK (menu_button_button_press_event_cb), menu_button);
394 }
395
396 GtkWidget *
397 gtk_menu_button_new (void)
398 {
399   return g_object_new (GTK_TYPE_MENU_BUTTON, NULL);
400 }
401
402 /* Callback for the "deactivate" signal on the pop-up menu.
403  * This is used so that we unset the state of the toggle button
404  * when the pop-up menu disappears.
405  */
406 static int
407 menu_deactivate_cb (GtkMenuShell    *menu_shell,
408                     GtkMenuButton *menu_button)
409 {
410   gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (menu_button), FALSE);
411
412   return TRUE;
413 }
414
415 static void
416 menu_detacher (GtkWidget *widget,
417                GtkMenu   *menu)
418 {
419   GtkMenuButtonPrivate *priv = GTK_MENU_BUTTON (widget)->priv;
420
421   g_return_if_fail (priv->menu == (GtkWidget *) menu);
422
423   priv->menu = NULL;
424 }
425
426 void
427 _gtk_menu_button_set_menu_with_func (GtkMenuButton                 *menu_button,
428                                      GtkWidget                     *menu,
429                                      GtkMenuButtonShowMenuCallback  func,
430                                      gpointer                       user_data)
431 {
432   GtkMenuButtonPrivate *priv;
433
434   g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
435   g_return_if_fail (GTK_IS_MENU (menu) || menu == NULL);
436
437   priv = menu_button->priv;
438   priv->func = func;
439   priv->user_data = user_data;
440
441   if (priv->menu == GTK_WIDGET (menu))
442     return;
443
444   if (priv->menu)
445     {
446       if (gtk_widget_get_visible (GTK_WIDGET (priv->menu)))
447         gtk_menu_shell_deactivate (GTK_MENU_SHELL (priv->menu));
448     }
449
450   if (priv->menu)
451     {
452       g_signal_handlers_disconnect_by_func (priv->menu,
453                                             menu_deactivate_cb,
454                                             menu_button);
455       gtk_menu_detach (GTK_MENU (priv->menu));
456     }
457
458   priv->menu = menu;
459
460   if (priv->menu)
461     {
462       gtk_menu_attach_to_widget (GTK_MENU (priv->menu), GTK_WIDGET (menu_button),
463                                  menu_detacher);
464
465       gtk_widget_set_sensitive (GTK_WIDGET (menu_button), TRUE);
466
467       g_signal_connect (priv->menu, "deactivate",
468                         G_CALLBACK (menu_deactivate_cb), menu_button);
469     }
470   else
471     {
472       gtk_widget_set_sensitive (GTK_WIDGET (menu_button), FALSE);
473     }
474
475   g_object_notify (G_OBJECT (menu_button), "menu");
476 }
477
478 void
479 gtk_menu_button_set_menu (GtkMenuButton *menu_button,
480                           GtkWidget     *menu)
481 {
482   _gtk_menu_button_set_menu_with_func (menu_button, menu, NULL, NULL);
483 }
484
485 void
486 gtk_menu_button_set_menu_model (GtkMenuButton *menu_button,
487                                 GMenuModel    *menu_model)
488 {
489   GtkMenuButtonPrivate *priv;
490   GtkWidget *menu;
491
492   g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
493   g_return_if_fail (G_IS_MENU_MODEL (menu_model) || menu_model == NULL);
494
495   priv = menu_button->priv;
496   g_clear_object (&priv->model);
497
498   if (menu_model == NULL)
499     {
500       gtk_menu_button_set_menu (menu_button, NULL);
501       return;
502     }
503
504   priv->model = g_object_ref (menu_model);
505   menu = gtk_menu_new_from_model (menu_model);
506   gtk_widget_show_all (menu);
507   gtk_menu_button_set_menu (menu_button, menu);
508
509   g_object_notify (G_OBJECT (menu_button), "model");
510 }
511
512 void
513 gtk_menu_button_set_parent (GtkMenuButton *menu_button,
514                             GtkWidget     *parent)
515 {
516   GtkMenuButtonPrivate *priv;
517
518   g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
519   g_return_if_fail (parent == NULL || gtk_widget_is_ancestor (GTK_WIDGET (menu_button), parent));
520
521   priv = menu_button->priv;
522   if (priv->parent == parent)
523     return;
524
525   g_clear_object (&priv->parent);
526
527   if (parent == NULL)
528     return;
529
530   priv->parent = g_object_ref (G_OBJECT (parent));
531
532   g_object_notify (G_OBJECT (menu_button), "parent");
533 }
534
535 void
536 gtk_menu_button_set_direction (GtkMenuButton *menu_button,
537                                  GtkArrowType     direction)
538 {
539   GtkMenuButtonPrivate *priv = menu_button->priv;
540   GtkWidget *arrow;
541
542   g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
543
544   if (priv->arrow_type == direction)
545     return;
546
547   priv->arrow_type = direction;
548   gtk_widget_destroy (gtk_bin_get_child (GTK_BIN (menu_button)));
549   arrow = gtk_arrow_new (priv->arrow_type, GTK_SHADOW_NONE);
550   gtk_container_add (GTK_CONTAINER (menu_button), arrow);
551   gtk_widget_show (arrow);
552 }
553
554 static void
555 gtk_menu_button_finalize (GObject *object)
556 {
557   GtkMenuButtonPrivate *priv = GTK_MENU_BUTTON (object)->priv;
558
559   if (priv->menu)
560     {
561       g_signal_handlers_disconnect_by_func (priv->menu,
562                                             menu_deactivate_cb,
563                                             object);
564       gtk_menu_detach (GTK_MENU (priv->menu));
565
566       g_signal_handlers_disconnect_by_func (object,
567                                             menu_button_toggled_cb,
568                                             object);
569       g_signal_handlers_disconnect_by_func (object,
570                                             menu_button_button_press_event_cb,
571                                             object);
572     }
573
574   G_OBJECT_CLASS (gtk_menu_button_parent_class)->finalize (object);
575 }