1 /* GTK - The GIMP Toolkit
2  * Copyright © 2014 Red Hat, Inc.
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "config.h"
19 #include "gtkpopovermenu.h"
20 #include "gtkstack.h"
21 #include "gtkstylecontext.h"
22 #include "gtkintl.h"
23 
24 
25 /**
26  * SECTION:gtkpopovermenu
27  * @Short_description: Popovers to use as menus
28  * @Title: GtkPopoverMenu
29  *
30  * GtkPopoverMenu is a subclass of #GtkPopover that treats its
31  * children like menus and allows switching between them. It is
32  * meant to be used primarily together with #GtkModelButton, but
33  * any widget can be used, such as #GtkSpinButton or #GtkScale.
34  * In this respect, GtkPopoverMenu is more flexible than popovers
35  * that are created from a #GMenuModel with gtk_popover_new_from_model().
36  *
37  * To add a child as a submenu, set the #GtkPopoverMenu:submenu
38  * child property to the name of the submenu. To let the user open
39  * this submenu, add a #GtkModelButton whose #GtkModelButton:menu-name
40  * property is set to the name you've given to the submenu.
41  *
42  * By convention, the first child of a submenu should be a #GtkModelButton
43  * to switch back to the parent menu. Such a button should use the
44  * #GtkModelButton:inverted and #GtkModelButton:centered properties
45  * to achieve a title-like appearance and place the submenu indicator
46  * at the opposite side. To switch back to the main menu, use "main"
47  * as the menu name.
48  *
49  * # Example
50  *
51  * |[<!-- language="xml" -->
52  * <object class="GtkPopoverMenu">
53  *   <child>
54  *     <object class="GtkBox">
55  *       <property name="visible">True</property>
56  *       <property name="margin">10</property>
57  *       <child>
58  *         <object class="GtkModelButton">
59  *           <property name="visible">True</property>
60  *           <property name="action-name">win.frob</property>
61  *           <property name="text" translatable="yes">Frob</property>
62  *         </object>
63  *       </child>
64  *       <child>
65  *         <object class="GtkModelButton">
66  *           <property name="visible">True</property>
67  *           <property name="menu-name">more</property>
68  *           <property name="text" translatable="yes">More</property>
69  *         </object>
70  *       </child>
71  *     </object>
72  *   </child>
73  *   <child>
74  *     <object class="GtkBox">
75  *       <property name="visible">True</property>
76  *       <property name="margin">10</property>
77  *       <child>
78  *         <object class="GtkModelButton">
79  *           <property name="visible">True</property>
80  *           <property name="action-name">win.foo</property>
81  *           <property name="text" translatable="yes">Foo</property>
82  *         </object>
83  *       </child>
84  *       <child>
85  *         <object class="GtkModelButton">
86  *           <property name="visible">True</property>
87  *           <property name="action-name">win.bar</property>
88  *           <property name="text" translatable="yes">Bar</property>
89  *         </object>
90  *       </child>
91  *     </object>
92  *     <packing>
93  *       <property name="submenu">more</property>
94  *     </packing>
95  *   </child>
96  * </object>
97  * ]|
98  *
99  * Just like normal popovers created using gtk_popover_new_from_model,
100  * #GtkPopoverMenu instances have a single css node called "popover"
101  * and get the .menu style class.
102  */
103 
104 struct _GtkPopoverMenu
105 {
106   GtkPopover parent_instance;
107 };
108 
109 enum {
110   PROP_VISIBLE_SUBMENU = 1
111 };
112 
113 enum {
114   CHILD_PROP_SUBMENU = 1,
115   CHILD_PROP_POSITION
116 };
117 
G_DEFINE_TYPE(GtkPopoverMenu,gtk_popover_menu,GTK_TYPE_POPOVER)118 G_DEFINE_TYPE (GtkPopoverMenu, gtk_popover_menu, GTK_TYPE_POPOVER)
119 
120 static void
121 visible_submenu_changed (GObject        *object,
122                          GParamSpec     *pspec,
123                          GtkPopoverMenu *popover)
124 {
125   g_object_notify (G_OBJECT (popover), "visible-submenu");
126 }
127 
128 static void
gtk_popover_menu_init(GtkPopoverMenu * popover)129 gtk_popover_menu_init (GtkPopoverMenu *popover)
130 {
131   GtkWidget *stack;
132   GtkStyleContext *style_context;
133 
134   stack = gtk_stack_new ();
135   gtk_stack_set_vhomogeneous (GTK_STACK (stack), FALSE);
136   gtk_stack_set_transition_type (GTK_STACK (stack), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
137   gtk_stack_set_interpolate_size (GTK_STACK (stack), TRUE);
138   gtk_widget_show (stack);
139   gtk_container_add (GTK_CONTAINER (popover), stack);
140   g_signal_connect (stack, "notify::visible-child-name",
141                     G_CALLBACK (visible_submenu_changed), popover);
142 
143   style_context = gtk_widget_get_style_context (GTK_WIDGET (popover));
144   gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_MENU);
145 }
146 
147 static void
gtk_popover_menu_map(GtkWidget * widget)148 gtk_popover_menu_map (GtkWidget *widget)
149 {
150   GTK_WIDGET_CLASS (gtk_popover_menu_parent_class)->map (widget);
151   gtk_popover_menu_open_submenu (GTK_POPOVER_MENU (widget), "main");
152 }
153 
154 static void
gtk_popover_menu_unmap(GtkWidget * widget)155 gtk_popover_menu_unmap (GtkWidget *widget)
156 {
157   gtk_popover_menu_open_submenu (GTK_POPOVER_MENU (widget), "main");
158   GTK_WIDGET_CLASS (gtk_popover_menu_parent_class)->unmap (widget);
159 }
160 
161 static void
gtk_popover_menu_add(GtkContainer * container,GtkWidget * child)162 gtk_popover_menu_add (GtkContainer *container,
163                       GtkWidget    *child)
164 {
165   GtkWidget *stack;
166 
167   stack = gtk_bin_get_child (GTK_BIN (container));
168 
169   if (stack == NULL)
170     {
171       gtk_widget_set_parent (child, GTK_WIDGET (container));
172       _gtk_bin_set_child (GTK_BIN (container), child);
173     }
174   else
175     {
176       gchar *name;
177 
178       if (gtk_stack_get_child_by_name (GTK_STACK (stack), "main"))
179         name = "submenu";
180       else
181         name = "main";
182 
183       gtk_stack_add_named (GTK_STACK (stack), child, name);
184     }
185 }
186 
187 static void
gtk_popover_menu_remove(GtkContainer * container,GtkWidget * child)188 gtk_popover_menu_remove (GtkContainer *container,
189                          GtkWidget    *child)
190 {
191   GtkWidget *stack;
192 
193   stack = gtk_bin_get_child (GTK_BIN (container));
194 
195   if (child == stack)
196     GTK_CONTAINER_CLASS (gtk_popover_menu_parent_class)->remove (container, child);
197   else
198     gtk_container_remove (GTK_CONTAINER (stack), child);
199 }
200 
201 static void
gtk_popover_menu_forall(GtkContainer * container,gboolean include_internals,GtkCallback callback,gpointer callback_data)202 gtk_popover_menu_forall (GtkContainer *container,
203                          gboolean      include_internals,
204                          GtkCallback   callback,
205                          gpointer      callback_data)
206 {
207   GtkWidget *stack;
208 
209   stack = gtk_bin_get_child (GTK_BIN (container));
210 
211   if (include_internals)
212     (* callback) (stack, callback_data);
213 
214   gtk_container_forall (GTK_CONTAINER (stack), callback, callback_data);
215 }
216 
217 static void
gtk_popover_menu_get_child_property(GtkContainer * container,GtkWidget * child,guint property_id,GValue * value,GParamSpec * pspec)218 gtk_popover_menu_get_child_property (GtkContainer *container,
219                                      GtkWidget    *child,
220                                      guint         property_id,
221                                      GValue       *value,
222                                      GParamSpec   *pspec)
223 {
224   GtkWidget *stack;
225 
226   stack = gtk_bin_get_child (GTK_BIN (container));
227 
228   if (child == stack)
229     return;
230 
231   switch (property_id)
232     {
233     case CHILD_PROP_SUBMENU:
234       {
235         gchar *name;
236         gtk_container_child_get (GTK_CONTAINER (stack), child, "name", &name, NULL);
237         g_value_set_string (value, name);
238       }
239       break;
240 
241     case CHILD_PROP_POSITION:
242       {
243         gint position;
244         gtk_container_child_get (GTK_CONTAINER (stack), child, "position", &position, NULL);
245         g_value_set_int (value, position);
246       }
247       break;
248 
249     default:
250       GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
251       break;
252     }
253 }
254 
255 static void
gtk_popover_menu_set_child_property(GtkContainer * container,GtkWidget * child,guint property_id,const GValue * value,GParamSpec * pspec)256 gtk_popover_menu_set_child_property (GtkContainer *container,
257                                      GtkWidget    *child,
258                                      guint         property_id,
259                                      const GValue *value,
260                                      GParamSpec   *pspec)
261 {
262   GtkWidget *stack;
263 
264   stack = gtk_bin_get_child (GTK_BIN (container));
265 
266   if (child == stack)
267     return;
268 
269   switch (property_id)
270     {
271     case CHILD_PROP_SUBMENU:
272       {
273         const gchar *name;
274         name = g_value_get_string (value);
275         gtk_container_child_set (GTK_CONTAINER (stack), child, "name", name, NULL);
276       }
277       break;
278 
279     case CHILD_PROP_POSITION:
280       {
281         gint position;
282         position = g_value_get_int (value);
283         gtk_container_child_set (GTK_CONTAINER (stack), child, "position", position, NULL);
284       }
285       break;
286 
287     default:
288       GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
289       break;
290     }
291 }
292 
293 static void
gtk_popover_menu_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)294 gtk_popover_menu_get_property (GObject    *object,
295                                guint       property_id,
296                                GValue     *value,
297                                GParamSpec *pspec)
298 {
299   GtkWidget *stack;
300 
301   stack = gtk_bin_get_child (GTK_BIN (object));
302 
303   switch (property_id)
304     {
305     case PROP_VISIBLE_SUBMENU:
306       g_value_set_string (value, gtk_stack_get_visible_child_name (GTK_STACK (stack)));
307       break;
308 
309     default:
310       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
311       break;
312     }
313 }
314 
315 static void
gtk_popover_menu_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)316 gtk_popover_menu_set_property (GObject      *object,
317                                guint         property_id,
318                                const GValue *value,
319                                GParamSpec   *pspec)
320 {
321   GtkWidget *stack;
322 
323   stack = gtk_bin_get_child (GTK_BIN (object));
324 
325   switch (property_id)
326     {
327     case PROP_VISIBLE_SUBMENU:
328       gtk_stack_set_visible_child_name (GTK_STACK (stack), g_value_get_string (value));
329       break;
330 
331     default:
332       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
333       break;
334     }
335 }
336 
337 static void
gtk_popover_menu_class_init(GtkPopoverMenuClass * klass)338 gtk_popover_menu_class_init (GtkPopoverMenuClass *klass)
339 {
340   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
341   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
342   GObjectClass *object_class = G_OBJECT_CLASS (klass);
343 
344   object_class->set_property = gtk_popover_menu_set_property;
345   object_class->get_property = gtk_popover_menu_get_property;
346 
347   widget_class->map = gtk_popover_menu_map;
348   widget_class->unmap = gtk_popover_menu_unmap;
349 
350   container_class->add = gtk_popover_menu_add;
351   container_class->remove = gtk_popover_menu_remove;
352   container_class->forall = gtk_popover_menu_forall;
353   container_class->set_child_property = gtk_popover_menu_set_child_property;
354   container_class->get_child_property = gtk_popover_menu_get_child_property;
355 
356   g_object_class_install_property (object_class,
357                                    PROP_VISIBLE_SUBMENU,
358                                    g_param_spec_string ("visible-submenu",
359                                                         P_("Visible submenu"),
360                                                         P_("The name of the visible submenu"),
361                                                         NULL,
362                                                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
363 
364   /**
365    * GtkPopoverMenu:submenu:
366    *
367    * The submenu child property specifies the name of the submenu
368    * If it is %NULL or "main", the child is used as the main menu,
369    * which is shown initially when the popover is mapped.
370    *
371    * Since: 3.16
372    */
373   gtk_container_class_install_child_property (container_class,
374                                               CHILD_PROP_SUBMENU,
375                                               g_param_spec_string ("submenu",
376                                                                    P_("Submenu"),
377                                                                    P_("The name of the submenu"),
378                                                                    NULL,
379                                                                    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
380 
381   gtk_container_class_install_child_property (container_class,
382                                               CHILD_PROP_POSITION,
383                                               g_param_spec_int ("position",
384                                                                 P_("Position"),
385                                                                 P_("The index of the child in the parent"),
386                                                                 -1, G_MAXINT, 0,
387                                                                 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
388 }
389 
390 /**
391  * gtk_popover_menu_new:
392  *
393  * Creates a new popover menu.
394  *
395  * Returns: a new #GtkPopoverMenu
396  *
397  * Since: 3.16
398  */
399 GtkWidget *
gtk_popover_menu_new(void)400 gtk_popover_menu_new (void)
401 {
402   return g_object_new (GTK_TYPE_POPOVER_MENU, NULL);
403 }
404 
405 /**
406  * gtk_popover_menu_open_submenu:
407  * @popover: a #GtkPopoverMenu
408  * @name: the name of the menu to switch to
409  *
410  * Opens a submenu of the @popover. The @name
411  * must be one of the names given to the submenus
412  * of @popover with #GtkPopoverMenu:submenu, or
413  * "main" to switch back to the main menu.
414  *
415  * #GtkModelButton will open submenus automatically
416  * when the #GtkModelButton:menu-name property is set,
417  * so this function is only needed when you are using
418  * other kinds of widgets to initiate menu changes.
419  *
420  * Since: 3.16
421  */
422 void
gtk_popover_menu_open_submenu(GtkPopoverMenu * popover,const gchar * name)423 gtk_popover_menu_open_submenu (GtkPopoverMenu *popover,
424                                const gchar    *name)
425 {
426   GtkWidget *stack;
427 
428   g_return_if_fail (GTK_IS_POPOVER_MENU (popover));
429 
430   stack = gtk_bin_get_child (GTK_BIN (popover));
431   gtk_stack_set_visible_child_name (GTK_STACK (stack), name);
432 }
433