1 | /* |
2 | * Copyright © 2014 Canonical Limited |
3 | * Copyright © 2013 Carlos Garnacho |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Lesser General Public |
7 | * License as published by the Free Software Foundation; either |
8 | * version 2 of the licence, or (at your option) any later version. |
9 | * |
10 | * This library is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | * Lesser General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Lesser General Public |
16 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
17 | * |
18 | * Author: Ryan Lortie <desrt@desrt.ca> |
19 | */ |
20 | |
21 | #include "config.h" |
22 | |
23 | #include "gtkmenusectionboxprivate.h" |
24 | |
25 | #include "gtkwidgetprivate.h" |
26 | #include "gtklabel.h" |
27 | #include "gtkmenutrackerprivate.h" |
28 | #include "gtkmodelbuttonprivate.h" |
29 | #include "gtkseparator.h" |
30 | #include "gtksizegroup.h" |
31 | #include "gtkstack.h" |
32 | #include "gtkpopovermenuprivate.h" |
33 | #include "gtkorientable.h" |
34 | #include "gtkbuiltiniconprivate.h" |
35 | #include "gtkgizmoprivate.h" |
36 | #include "gtkbinlayout.h" |
37 | #include "gtkprivate.h" |
38 | |
39 | typedef GtkBoxClass ; |
40 | |
41 | struct |
42 | { |
43 | GtkBox ; |
44 | |
45 | GtkMenuSectionBox *; |
46 | GtkMenuTracker *; |
47 | GtkBox *; |
48 | GtkWidget *; |
49 | guint ; |
50 | gboolean ; |
51 | gboolean ; |
52 | gboolean ; |
53 | int ; |
54 | GtkPopoverMenuFlags ; |
55 | GtkSizeGroup *; |
56 | GHashTable *; |
57 | }; |
58 | |
59 | typedef struct |
60 | { |
61 | int ; |
62 | gboolean ; |
63 | } ; |
64 | |
65 | G_DEFINE_TYPE (GtkMenuSectionBox, gtk_menu_section_box, GTK_TYPE_BOX) |
66 | |
67 | static void gtk_menu_section_box_sync_separators (GtkMenuSectionBox *box, |
68 | MenuData *data); |
69 | static void gtk_menu_section_box_new_submenu (GtkMenuTrackerItem *item, |
70 | GtkMenuSectionBox *toplevel, |
71 | GtkWidget *focus, |
72 | const char *name); |
73 | static GtkWidget * gtk_menu_section_box_new_section (GtkMenuTrackerItem *item, |
74 | GtkMenuSectionBox *parent); |
75 | |
76 | /* We are trying to implement the following rules here: |
77 | * |
78 | * rule 1: never ever show separators for empty sections |
79 | * rule 2: always show a separator if there is a label |
80 | * rule 3: don't show a separator for the first section |
81 | * rule 4: don't show a separator for the following sections if there are |
82 | * no items before it |
83 | * rule 5: never show separators directly above or below an iconic box |
84 | * (rule 6: these rules don't apply exactly the same way for subsections) |
85 | */ |
86 | static void |
87 | (GtkMenuSectionBox *box, |
88 | MenuData *data) |
89 | { |
90 | gboolean previous_section_is_iconic; |
91 | gboolean should_have_separator; |
92 | gboolean should_have_top_margin = FALSE; |
93 | gboolean is_not_empty_item; |
94 | gboolean has_separator; |
95 | gboolean has_label; |
96 | gboolean separator_condition; |
97 | int n_items_before; |
98 | GtkWidget *child; |
99 | |
100 | n_items_before = data->n_items; |
101 | previous_section_is_iconic = data->previous_is_iconic; |
102 | |
103 | for (child = gtk_widget_get_first_child (GTK_WIDGET (box->item_box)); |
104 | child != NULL; |
105 | child = gtk_widget_get_next_sibling (widget: child)) |
106 | { |
107 | if (GTK_IS_MENU_SECTION_BOX (child)) |
108 | gtk_menu_section_box_sync_separators (GTK_MENU_SECTION_BOX (child), data); |
109 | else |
110 | data->n_items++; |
111 | } |
112 | |
113 | is_not_empty_item = (data->n_items > n_items_before); |
114 | |
115 | if (is_not_empty_item) |
116 | data->previous_is_iconic = box->iconic; |
117 | |
118 | if (box->separator == NULL) |
119 | return; |
120 | |
121 | has_separator = gtk_widget_get_parent (widget: box->separator) != NULL; |
122 | has_label = !GTK_IS_SEPARATOR (box->separator); |
123 | |
124 | separator_condition = has_label ? TRUE : n_items_before > 0 && |
125 | box->depth <= 1 && |
126 | !previous_section_is_iconic && |
127 | !box->iconic; |
128 | |
129 | should_have_separator = separator_condition && is_not_empty_item; |
130 | |
131 | should_have_top_margin = !should_have_separator && |
132 | (box->depth <= 1 || box->iconic || box->circular) && |
133 | n_items_before > 0 && |
134 | is_not_empty_item; |
135 | |
136 | gtk_widget_set_margin_top (GTK_WIDGET (box->item_box), margin: should_have_top_margin ? 10 : 0); |
137 | |
138 | if (has_label) |
139 | { |
140 | GtkWidget *separator = gtk_widget_get_first_child (widget: box->separator); |
141 | |
142 | gtk_widget_set_visible (widget: separator, visible: n_items_before > 0); |
143 | } |
144 | |
145 | if (should_have_separator == has_separator) |
146 | return; |
147 | |
148 | if (should_have_separator) |
149 | gtk_box_insert_child_after (GTK_BOX (box), child: box->separator, NULL); |
150 | else |
151 | gtk_box_remove (GTK_BOX (box), child: box->separator); |
152 | } |
153 | |
154 | static gboolean |
155 | gtk_menu_section_box_handle_sync_separators (gpointer user_data) |
156 | { |
157 | GtkMenuSectionBox *box = user_data; |
158 | MenuData data; |
159 | |
160 | data.n_items = 0; |
161 | data.previous_is_iconic = FALSE; |
162 | gtk_menu_section_box_sync_separators (box, data: &data); |
163 | |
164 | box->separator_sync_idle = 0; |
165 | |
166 | return G_SOURCE_REMOVE; |
167 | } |
168 | |
169 | static void |
170 | (GtkMenuSectionBox *box) |
171 | { |
172 | box = box->toplevel; |
173 | |
174 | if (!box->separator_sync_idle) |
175 | { |
176 | box->separator_sync_idle = g_idle_add_full (G_PRIORITY_HIGH_IDLE, /* before resize... */ |
177 | function: gtk_menu_section_box_handle_sync_separators, |
178 | data: box, NULL); |
179 | gdk_source_set_static_name_by_id (tag: box->separator_sync_idle, name: "[gtk] menu section box handle sync separators" ); |
180 | } |
181 | } |
182 | |
183 | static void |
184 | gtk_popover_item_activate (GtkWidget *button, |
185 | gpointer user_data) |
186 | { |
187 | GtkMenuTrackerItem *item = user_data; |
188 | GtkWidget *popover = NULL; |
189 | |
190 | if (gtk_menu_tracker_item_get_role (self: item) == GTK_MENU_TRACKER_ITEM_ROLE_NORMAL) |
191 | { |
192 | /* Activating the item could cause the popover |
193 | * to be free'd, for example if it is a Quit item |
194 | */ |
195 | popover = gtk_widget_get_ancestor (widget: button, GTK_TYPE_POPOVER); |
196 | if (popover) |
197 | g_object_ref (popover); |
198 | } |
199 | |
200 | gtk_menu_tracker_item_activated (self: item); |
201 | |
202 | if (popover != NULL) |
203 | { |
204 | gtk_widget_hide (widget: popover); |
205 | g_object_unref (object: popover); |
206 | } |
207 | } |
208 | |
209 | static void |
210 | (int position, |
211 | gpointer user_data) |
212 | { |
213 | GtkMenuSectionBox *box = user_data; |
214 | GtkMenuTrackerItem *item; |
215 | GtkWidget *widget; |
216 | int pos; |
217 | |
218 | for (widget = gtk_widget_get_first_child (GTK_WIDGET (box->item_box)), pos = 0; |
219 | widget != NULL; |
220 | widget = gtk_widget_get_next_sibling (widget), pos++) |
221 | { |
222 | if (pos == position) |
223 | break; |
224 | } |
225 | |
226 | item = g_object_get_data (G_OBJECT (widget), key: "GtkMenuTrackerItem" ); |
227 | if (gtk_menu_tracker_item_get_has_link (self: item, G_MENU_LINK_SUBMENU)) |
228 | { |
229 | GtkWidget *stack, *subbox; |
230 | |
231 | stack = gtk_widget_get_ancestor (GTK_WIDGET (box->toplevel), GTK_TYPE_STACK); |
232 | subbox = gtk_stack_get_child_by_name (GTK_STACK (stack), name: gtk_menu_tracker_item_get_label (self: item)); |
233 | if (subbox != NULL) |
234 | gtk_stack_remove (GTK_STACK (stack), child: subbox); |
235 | } |
236 | |
237 | gtk_box_remove (GTK_BOX (box->item_box), child: widget); |
238 | |
239 | gtk_menu_section_box_schedule_separator_sync (box); |
240 | } |
241 | |
242 | static gboolean |
243 | get_ancestors (GtkWidget *widget, |
244 | GType widget_type, |
245 | GtkWidget **ancestor, |
246 | GtkWidget **below) |
247 | { |
248 | GtkWidget *a, *b; |
249 | |
250 | a = NULL; |
251 | b = widget; |
252 | while (b != NULL) |
253 | { |
254 | a = gtk_widget_get_parent (widget: b); |
255 | if (!a) |
256 | return FALSE; |
257 | if (g_type_is_a (G_OBJECT_TYPE (a), is_a_type: widget_type)) |
258 | break; |
259 | b = a; |
260 | } |
261 | |
262 | *below = b; |
263 | *ancestor = a; |
264 | |
265 | return TRUE; |
266 | } |
267 | |
268 | static void |
269 | (GtkWidget *button, |
270 | gpointer data) |
271 | { |
272 | GtkMenuTrackerItem *item = data; |
273 | GtkWidget *focus; |
274 | |
275 | if (gtk_menu_tracker_item_get_should_request_show (self: item)) |
276 | gtk_menu_tracker_item_request_submenu_shown (self: item, FALSE); |
277 | |
278 | focus = GTK_WIDGET (g_object_get_data (G_OBJECT (button), "focus" )); |
279 | gtk_widget_grab_focus (widget: focus); |
280 | } |
281 | |
282 | static void |
283 | (GtkWidget *button, |
284 | gpointer data) |
285 | { |
286 | GtkMenuTrackerItem *item = data; |
287 | GtkWidget *focus; |
288 | |
289 | if (gtk_menu_tracker_item_get_should_request_show (self: item)) |
290 | gtk_menu_tracker_item_request_submenu_shown (self: item, TRUE); |
291 | |
292 | focus = GTK_WIDGET (g_object_get_data (G_OBJECT (button), "focus" )); |
293 | gtk_widget_grab_focus (widget: focus); |
294 | } |
295 | |
296 | static void |
297 | (GtkPopoverMenu *popover, |
298 | GtkMenuTrackerItem *item) |
299 | { |
300 | if (gtk_menu_tracker_item_get_should_request_show (self: item)) |
301 | gtk_menu_tracker_item_request_submenu_shown (self: item, TRUE); |
302 | } |
303 | |
304 | static void |
305 | (GtkPopoverMenu *popover, |
306 | GtkMenuTrackerItem *item) |
307 | { |
308 | if (gtk_menu_tracker_item_get_should_request_show (self: item)) |
309 | gtk_menu_tracker_item_request_submenu_shown (self: item, FALSE); |
310 | } |
311 | |
312 | /* We're using the gizmo as an easy single child container, not as |
313 | * a custom widget to draw some visual indicators (like markers). |
314 | * As such its default focus functions blocks focus through the children, |
315 | * so we need to handle it correctly here so that custom widgets inside |
316 | * menus can be focused. |
317 | */ |
318 | static gboolean |
319 | custom_widget_focus (GtkGizmo *gizmo, |
320 | GtkDirectionType direction) |
321 | { |
322 | return gtk_widget_focus_child (GTK_WIDGET (gizmo), direction); |
323 | } |
324 | |
325 | static gboolean |
326 | custom_widget_grab_focus (GtkGizmo *gizmo) |
327 | { |
328 | return gtk_widget_grab_focus_child (GTK_WIDGET (gizmo)); |
329 | } |
330 | |
331 | static void |
332 | (GtkMenuTrackerItem *item, |
333 | int position, |
334 | gpointer user_data) |
335 | { |
336 | GtkMenuSectionBox *box = user_data; |
337 | GtkWidget *widget; |
338 | |
339 | if (gtk_menu_tracker_item_get_is_separator (self: item)) |
340 | { |
341 | widget = gtk_menu_section_box_new_section (item, parent: box); |
342 | } |
343 | else if (gtk_menu_tracker_item_get_has_link (self: item, G_MENU_LINK_SUBMENU)) |
344 | { |
345 | if (box->flags & GTK_POPOVER_MENU_NESTED) |
346 | { |
347 | GMenuModel *model; |
348 | GtkWidget *; |
349 | |
350 | model = _gtk_menu_tracker_item_get_link (self: item, G_MENU_LINK_SUBMENU); |
351 | |
352 | submenu = gtk_popover_menu_new_from_model_full (model, flags: box->flags); |
353 | gtk_popover_set_has_arrow (GTK_POPOVER (submenu), FALSE); |
354 | gtk_widget_set_valign (widget: submenu, align: GTK_ALIGN_START); |
355 | |
356 | widget = g_object_new (GTK_TYPE_MODEL_BUTTON, |
357 | first_property_name: "popover" , submenu, |
358 | "indicator-size-group" , box->indicators, |
359 | NULL); |
360 | g_object_bind_property (source: item, source_property: "label" , target: widget, target_property: "text" , flags: G_BINDING_SYNC_CREATE); |
361 | g_object_bind_property (source: item, source_property: "icon" , target: widget, target_property: "icon" , flags: G_BINDING_SYNC_CREATE); |
362 | g_object_bind_property (source: item, source_property: "sensitive" , target: widget, target_property: "sensitive" , flags: G_BINDING_SYNC_CREATE); |
363 | |
364 | g_signal_connect (submenu, "show" , G_CALLBACK (submenu_shown), item); |
365 | g_signal_connect (submenu, "hide" , G_CALLBACK (submenu_hidden), item); |
366 | } |
367 | else |
368 | { |
369 | GtkWidget *stack = NULL; |
370 | GtkWidget *parent = NULL; |
371 | char *name; |
372 | |
373 | widget = g_object_new (GTK_TYPE_MODEL_BUTTON, |
374 | first_property_name: "menu-name" , gtk_menu_tracker_item_get_label (self: item), |
375 | "indicator-size-group" , box->indicators, |
376 | NULL); |
377 | g_object_bind_property (source: item, source_property: "label" , target: widget, target_property: "text" , flags: G_BINDING_SYNC_CREATE); |
378 | g_object_bind_property (source: item, source_property: "icon" , target: widget, target_property: "icon" , flags: G_BINDING_SYNC_CREATE); |
379 | g_object_bind_property (source: item, source_property: "sensitive" , target: widget, target_property: "sensitive" , flags: G_BINDING_SYNC_CREATE); |
380 | |
381 | get_ancestors (GTK_WIDGET (box->toplevel), GTK_TYPE_STACK, ancestor: &stack, below: &parent); |
382 | g_object_get (object: gtk_stack_get_page (GTK_STACK (stack), child: parent), first_property_name: "name" , &name, NULL); |
383 | gtk_menu_section_box_new_submenu (item, toplevel: box->toplevel, focus: widget, name); |
384 | g_free (mem: name); |
385 | } |
386 | } |
387 | else if (gtk_menu_tracker_item_get_custom (self: item)) |
388 | { |
389 | const char *id = gtk_menu_tracker_item_get_custom (self: item); |
390 | |
391 | widget = gtk_gizmo_new (css_name: "widget" , NULL, NULL, NULL, NULL, focus_func: custom_widget_focus, grab_focus_func: custom_widget_grab_focus); |
392 | gtk_widget_set_layout_manager (widget, layout_manager: gtk_bin_layout_new ()); |
393 | |
394 | if (g_hash_table_lookup (hash_table: box->custom_slots, key: id)) |
395 | g_warning ("Duplicate custom ID: %s" , id); |
396 | else |
397 | { |
398 | char *slot_id = g_strdup (str: id); |
399 | g_object_set_data_full (G_OBJECT (widget), key: "slot-id" , data: slot_id, destroy: g_free); |
400 | g_hash_table_insert (hash_table: box->custom_slots, key: slot_id, value: widget); |
401 | } |
402 | } |
403 | else |
404 | { |
405 | widget = g_object_new (GTK_TYPE_MODEL_BUTTON, |
406 | first_property_name: "indicator-size-group" , box->indicators, |
407 | NULL); |
408 | g_object_bind_property (source: item, source_property: "label" , target: widget, target_property: "text" , flags: G_BINDING_SYNC_CREATE); |
409 | |
410 | if (box->iconic) |
411 | { |
412 | g_object_bind_property (source: item, source_property: "verb-icon" , target: widget, target_property: "icon" , flags: G_BINDING_SYNC_CREATE); |
413 | g_object_set (object: widget, first_property_name: "iconic" , TRUE, NULL); |
414 | } |
415 | else if (box->inline_buttons) |
416 | { |
417 | g_object_bind_property (source: item, source_property: "verb-icon" , target: widget, target_property: "icon" , flags: G_BINDING_SYNC_CREATE); |
418 | g_object_set (object: widget, first_property_name: "iconic" , TRUE, NULL); |
419 | gtk_widget_add_css_class (widget, css_class: "flat" ); |
420 | } |
421 | else if (box->circular) |
422 | { |
423 | g_object_bind_property (source: item, source_property: "verb-icon" , target: widget, target_property: "icon" , flags: G_BINDING_SYNC_CREATE); |
424 | g_object_set (object: widget, first_property_name: "iconic" , TRUE, NULL); |
425 | gtk_widget_add_css_class (widget, css_class: "circular" ); |
426 | } |
427 | else |
428 | g_object_bind_property (source: item, source_property: "icon" , target: widget, target_property: "icon" , flags: G_BINDING_SYNC_CREATE); |
429 | |
430 | g_object_bind_property (source: item, source_property: "use-markup" , target: widget, target_property: "use-markup" , flags: G_BINDING_SYNC_CREATE); |
431 | g_object_bind_property (source: item, source_property: "sensitive" , target: widget, target_property: "sensitive" , flags: G_BINDING_SYNC_CREATE); |
432 | g_object_bind_property (source: item, source_property: "role" , target: widget, target_property: "role" , flags: G_BINDING_SYNC_CREATE); |
433 | g_object_bind_property (source: item, source_property: "toggled" , target: widget, target_property: "active" , flags: G_BINDING_SYNC_CREATE); |
434 | g_object_bind_property (source: item, source_property: "accel" , target: widget, target_property: "accel" , flags: G_BINDING_SYNC_CREATE); |
435 | g_signal_connect (widget, "clicked" , G_CALLBACK (gtk_popover_item_activate), item); |
436 | } |
437 | |
438 | g_object_set_data_full (G_OBJECT (widget), key: "GtkMenuTrackerItem" , g_object_ref (item), destroy: g_object_unref); |
439 | |
440 | if (box->circular) |
441 | { |
442 | gtk_widget_set_hexpand (widget, TRUE); |
443 | gtk_widget_set_halign (widget, align: GTK_ALIGN_CENTER); |
444 | } |
445 | else |
446 | { |
447 | gtk_widget_set_halign (widget, align: GTK_ALIGN_FILL); |
448 | } |
449 | gtk_box_append (GTK_BOX (box->item_box), child: widget); |
450 | |
451 | if (position == 0) |
452 | gtk_box_reorder_child_after (GTK_BOX (box->item_box), child: widget, NULL); |
453 | else |
454 | { |
455 | GtkWidget *sibling = gtk_widget_get_first_child (GTK_WIDGET (box->item_box)); |
456 | int i; |
457 | for (i = 1; i < position; i++) |
458 | sibling = gtk_widget_get_next_sibling (widget: sibling); |
459 | gtk_box_reorder_child_after (GTK_BOX (box->item_box), child: widget, sibling); |
460 | } |
461 | |
462 | if (box->circular) |
463 | { |
464 | GtkWidget *c1, *c2, *c3; |
465 | |
466 | /* special-case the n > 2 case */ |
467 | c1 = gtk_widget_get_first_child (GTK_WIDGET (box->item_box)); |
468 | if ((c2 = gtk_widget_get_next_sibling (widget: c1)) != NULL && |
469 | (c3 = gtk_widget_get_next_sibling (widget: c2)) != NULL) |
470 | { |
471 | gtk_widget_set_halign (widget: c1, align: GTK_ALIGN_START); |
472 | while (c3 != NULL) |
473 | { |
474 | gtk_widget_set_halign (widget: c2, align: GTK_ALIGN_CENTER); |
475 | c2 = c3; |
476 | c3 = gtk_widget_get_next_sibling (widget: c3); |
477 | } |
478 | gtk_widget_set_halign (widget: c2, align: GTK_ALIGN_END); |
479 | } |
480 | } |
481 | |
482 | gtk_menu_section_box_schedule_separator_sync (box); |
483 | } |
484 | |
485 | static void |
486 | (GtkMenuSectionBox *box) |
487 | { |
488 | GtkWidget *item_box; |
489 | |
490 | gtk_orientable_set_orientation (GTK_ORIENTABLE (box), orientation: GTK_ORIENTATION_VERTICAL); |
491 | |
492 | box->toplevel = box; |
493 | |
494 | item_box = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 0); |
495 | box->item_box = GTK_BOX (item_box); |
496 | gtk_box_append (GTK_BOX (box), child: item_box); |
497 | gtk_widget_set_halign (GTK_WIDGET (item_box), align: GTK_ALIGN_FILL); |
498 | gtk_widget_set_halign (GTK_WIDGET (box), align: GTK_ALIGN_FILL); |
499 | } |
500 | |
501 | static void |
502 | (GObject *object) |
503 | { |
504 | GtkMenuSectionBox *box = GTK_MENU_SECTION_BOX (object); |
505 | |
506 | if (box->separator_sync_idle) |
507 | { |
508 | g_source_remove (tag: box->separator_sync_idle); |
509 | box->separator_sync_idle = 0; |
510 | } |
511 | |
512 | g_clear_object (&box->separator); |
513 | |
514 | if (box->tracker) |
515 | { |
516 | gtk_menu_tracker_free (tracker: box->tracker); |
517 | box->tracker = NULL; |
518 | } |
519 | |
520 | g_clear_object (&box->indicators); |
521 | g_clear_pointer (&box->custom_slots, g_hash_table_unref); |
522 | |
523 | G_OBJECT_CLASS (gtk_menu_section_box_parent_class)->dispose (object); |
524 | } |
525 | |
526 | static void |
527 | (GtkMenuSectionBoxClass *class) |
528 | { |
529 | G_OBJECT_CLASS (class)->dispose = gtk_menu_section_box_dispose; |
530 | } |
531 | |
532 | static void |
533 | update_popover_position_cb (GObject *source, |
534 | GParamSpec *spec, |
535 | gpointer *user_data) |
536 | { |
537 | GtkPopover *popover = GTK_POPOVER (source); |
538 | GtkMenuSectionBox *box = GTK_MENU_SECTION_BOX (user_data); |
539 | GtkWidget *w; |
540 | |
541 | GtkPositionType new_pos = gtk_popover_get_position (popover); |
542 | |
543 | for (w = gtk_widget_get_first_child (widget: gtk_widget_get_parent (GTK_WIDGET (box))); |
544 | w != NULL; |
545 | w = gtk_widget_get_next_sibling (widget: w)) |
546 | { |
547 | if (new_pos == GTK_POS_BOTTOM) |
548 | gtk_widget_set_valign (widget: w, align: GTK_ALIGN_START); |
549 | else if (new_pos == GTK_POS_TOP) |
550 | gtk_widget_set_valign (widget: w, align: GTK_ALIGN_END); |
551 | else |
552 | gtk_widget_set_valign (widget: w, align: GTK_ALIGN_CENTER); |
553 | } |
554 | } |
555 | |
556 | void |
557 | (GtkPopoverMenu *popover, |
558 | GMenuModel *model, |
559 | GtkPopoverMenuFlags flags) |
560 | { |
561 | GtkMenuSectionBox *box; |
562 | |
563 | box = g_object_new (GTK_TYPE_MENU_SECTION_BOX, NULL); |
564 | box->indicators = gtk_size_group_new (mode: GTK_SIZE_GROUP_HORIZONTAL); |
565 | box->custom_slots = g_hash_table_new (hash_func: g_str_hash, key_equal_func: g_str_equal); |
566 | box->flags = flags; |
567 | |
568 | gtk_popover_menu_add_submenu (popover, GTK_WIDGET (box), name: "main" ); |
569 | |
570 | box->tracker = gtk_menu_tracker_new (GTK_ACTION_OBSERVABLE (_gtk_widget_get_action_muxer (GTK_WIDGET (box), TRUE)), |
571 | model, TRUE, FALSE, FALSE, NULL, |
572 | insert_func: gtk_menu_section_box_insert_func, |
573 | remove_func: gtk_menu_section_box_remove_func, user_data: box); |
574 | |
575 | g_signal_connect (G_OBJECT (popover), "notify::position" , G_CALLBACK (update_popover_position_cb), box); |
576 | } |
577 | |
578 | static void |
579 | (GtkMenuTrackerItem *item, |
580 | GtkMenuSectionBox *toplevel, |
581 | GtkWidget *focus, |
582 | const char *name) |
583 | { |
584 | GtkMenuSectionBox *box; |
585 | GtkWidget *button; |
586 | |
587 | box = g_object_new (GTK_TYPE_MENU_SECTION_BOX, NULL); |
588 | box->indicators = gtk_size_group_new (mode: GTK_SIZE_GROUP_HORIZONTAL); |
589 | box->custom_slots = g_hash_table_ref (hash_table: toplevel->custom_slots); |
590 | box->flags = toplevel->flags; |
591 | |
592 | button = g_object_new (GTK_TYPE_MODEL_BUTTON, |
593 | first_property_name: "menu-name" , name, |
594 | "role" , GTK_BUTTON_ROLE_TITLE, |
595 | NULL); |
596 | |
597 | g_object_bind_property (source: item, source_property: "label" , target: button, target_property: "text" , flags: G_BINDING_SYNC_CREATE); |
598 | g_object_bind_property (source: item, source_property: "icon" , target: button, target_property: "icon" , flags: G_BINDING_SYNC_CREATE); |
599 | |
600 | g_object_set_data (G_OBJECT (button), key: "focus" , data: focus); |
601 | g_object_set_data (G_OBJECT (focus), key: "focus" , data: button); |
602 | |
603 | gtk_box_insert_child_after (GTK_BOX (box), child: button, NULL); |
604 | |
605 | g_signal_connect (focus, "clicked" , G_CALLBACK (open_submenu), item); |
606 | g_signal_connect (button, "clicked" , G_CALLBACK (close_submenu), item); |
607 | |
608 | gtk_stack_add_named (GTK_STACK (gtk_widget_get_ancestor (GTK_WIDGET (toplevel), GTK_TYPE_STACK)), |
609 | GTK_WIDGET (box), name: gtk_menu_tracker_item_get_label (self: item)); |
610 | |
611 | box->tracker = gtk_menu_tracker_new_for_item_link (item, G_MENU_LINK_SUBMENU, FALSE, FALSE, |
612 | insert_func: gtk_menu_section_box_insert_func, |
613 | remove_func: gtk_menu_section_box_remove_func, |
614 | user_data: box); |
615 | } |
616 | |
617 | static GtkWidget * |
618 | (GtkMenuTrackerItem *item, |
619 | GtkMenuSectionBox *parent) |
620 | { |
621 | GtkMenuSectionBox *box; |
622 | const char *label; |
623 | const char *hint; |
624 | const char *text_direction; |
625 | |
626 | box = g_object_new (GTK_TYPE_MENU_SECTION_BOX, NULL); |
627 | box->indicators = g_object_ref (parent->indicators); |
628 | box->custom_slots = g_hash_table_ref (hash_table: parent->toplevel->custom_slots); |
629 | box->toplevel = parent->toplevel; |
630 | box->depth = parent->depth + 1; |
631 | box->flags = parent->flags; |
632 | |
633 | label = gtk_menu_tracker_item_get_label (self: item); |
634 | hint = gtk_menu_tracker_item_get_display_hint (self: item); |
635 | text_direction = gtk_menu_tracker_item_get_text_direction (self: item); |
636 | |
637 | if (hint && g_str_equal (v1: hint, v2: "horizontal-buttons" )) |
638 | { |
639 | gtk_box_set_homogeneous (box: box->item_box, TRUE); |
640 | gtk_orientable_set_orientation (GTK_ORIENTABLE (box->item_box), orientation: GTK_ORIENTATION_HORIZONTAL); |
641 | gtk_widget_add_css_class (GTK_WIDGET (box->item_box), css_class: "linked" ); |
642 | gtk_widget_add_css_class (GTK_WIDGET (box->item_box), css_class: "horizontal-buttons" ); |
643 | box->iconic = TRUE; |
644 | |
645 | if (text_direction) |
646 | { |
647 | GtkTextDirection dir = GTK_TEXT_DIR_NONE; |
648 | |
649 | if (g_str_equal (v1: text_direction, v2: "rtl" )) |
650 | dir = GTK_TEXT_DIR_RTL; |
651 | else if (g_str_equal (v1: text_direction, v2: "ltr" )) |
652 | dir = GTK_TEXT_DIR_LTR; |
653 | |
654 | gtk_widget_set_direction (GTK_WIDGET (box->item_box), dir); |
655 | } |
656 | } |
657 | else if (hint && g_str_equal (v1: hint, v2: "inline-buttons" )) |
658 | { |
659 | GtkWidget *item_box; |
660 | GtkWidget *spacer; |
661 | |
662 | box->inline_buttons = TRUE; |
663 | |
664 | gtk_orientable_set_orientation (GTK_ORIENTABLE (box->item_box), orientation: GTK_ORIENTATION_HORIZONTAL); |
665 | gtk_widget_add_css_class (GTK_WIDGET (box->item_box), css_class: "inline-buttons" ); |
666 | |
667 | spacer = gtk_builtin_icon_new (css_name: "none" ); |
668 | gtk_box_append (GTK_BOX (box->item_box), child: spacer); |
669 | gtk_size_group_add_widget (size_group: box->indicators, widget: spacer); |
670 | |
671 | if (label != NULL) |
672 | { |
673 | GtkWidget *title; |
674 | |
675 | title = gtk_label_new (str: label); |
676 | gtk_widget_set_hexpand (widget: title, TRUE); |
677 | gtk_widget_set_halign (widget: title, align: GTK_ALIGN_START); |
678 | g_object_bind_property (source: item, source_property: "label" , target: title, target_property: "label" , flags: G_BINDING_SYNC_CREATE); |
679 | gtk_box_append (GTK_BOX (box->item_box), child: title); |
680 | } |
681 | |
682 | item_box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 0); |
683 | gtk_box_append (GTK_BOX (box->item_box), child: item_box); |
684 | box->item_box = GTK_BOX (item_box); |
685 | } |
686 | else if (hint && g_str_equal (v1: hint, v2: "circular-buttons" )) |
687 | { |
688 | gtk_box_set_homogeneous (box: box->item_box, TRUE); |
689 | gtk_orientable_set_orientation (GTK_ORIENTABLE (box->item_box), orientation: GTK_ORIENTATION_HORIZONTAL); |
690 | gtk_widget_add_css_class (GTK_WIDGET (box->item_box), css_class: "circular-buttons" ); |
691 | box->circular = TRUE; |
692 | } |
693 | |
694 | if (label != NULL && !box->inline_buttons) |
695 | { |
696 | GtkWidget *separator; |
697 | GtkWidget *title; |
698 | |
699 | box->separator = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 0); |
700 | g_object_ref_sink (box->separator); |
701 | |
702 | separator = gtk_separator_new (orientation: GTK_ORIENTATION_HORIZONTAL); |
703 | gtk_widget_set_valign (widget: separator, align: GTK_ALIGN_CENTER); |
704 | gtk_widget_set_hexpand (widget: separator, TRUE); |
705 | gtk_box_append (GTK_BOX (box->separator), child: separator); |
706 | |
707 | title = gtk_label_new (str: label); |
708 | g_object_bind_property (source: item, source_property: "label" , target: title, target_property: "label" , flags: G_BINDING_SYNC_CREATE); |
709 | gtk_widget_add_css_class (widget: title, css_class: "separator" ); |
710 | gtk_widget_set_halign (widget: title, align: GTK_ALIGN_START); |
711 | gtk_label_set_xalign (GTK_LABEL (title), xalign: 0.0); |
712 | gtk_widget_add_css_class (widget: title, css_class: "title" ); |
713 | gtk_box_append (GTK_BOX (box->separator), child: title); |
714 | } |
715 | else |
716 | { |
717 | box->separator = gtk_separator_new (orientation: GTK_ORIENTATION_HORIZONTAL); |
718 | g_object_ref_sink (box->separator); |
719 | } |
720 | |
721 | box->tracker = gtk_menu_tracker_new_for_item_link (item, G_MENU_LINK_SECTION, FALSE, FALSE, |
722 | insert_func: gtk_menu_section_box_insert_func, |
723 | remove_func: gtk_menu_section_box_remove_func, |
724 | user_data: box); |
725 | |
726 | return GTK_WIDGET (box); |
727 | } |
728 | |
729 | gboolean |
730 | (GtkPopoverMenu *popover, |
731 | GtkWidget *child, |
732 | const char *id) |
733 | { |
734 | GtkWidget *stack; |
735 | GtkMenuSectionBox *box; |
736 | GtkWidget *slot; |
737 | |
738 | stack = gtk_popover_menu_get_stack (menu: popover); |
739 | box = GTK_MENU_SECTION_BOX (gtk_stack_get_child_by_name (GTK_STACK (stack), "main" )); |
740 | if (box == NULL) |
741 | return FALSE; |
742 | |
743 | slot = (GtkWidget *)g_hash_table_lookup (hash_table: box->custom_slots, key: id); |
744 | |
745 | if (slot == NULL) |
746 | return FALSE; |
747 | |
748 | if (gtk_widget_get_first_child (widget: slot)) |
749 | return FALSE; |
750 | |
751 | gtk_widget_insert_before (widget: child, parent: slot, NULL); |
752 | return TRUE; |
753 | } |
754 | |
755 | gboolean |
756 | (GtkPopoverMenu *popover, |
757 | GtkWidget *child) |
758 | { |
759 | GtkWidget *stack; |
760 | GtkMenuSectionBox *box; |
761 | GtkWidget *parent; |
762 | const char *id; |
763 | GtkWidget *slot; |
764 | |
765 | stack = gtk_popover_menu_get_stack (menu: popover); |
766 | box = GTK_MENU_SECTION_BOX (gtk_stack_get_child_by_name (GTK_STACK (stack), "main" )); |
767 | if (box == NULL) |
768 | return FALSE; |
769 | |
770 | parent = gtk_widget_get_parent (widget: child); |
771 | |
772 | id = (const char *) g_object_get_data (G_OBJECT (parent), key: "slot-id" ); |
773 | g_return_val_if_fail (id != NULL, FALSE); |
774 | |
775 | slot = (GtkWidget *)g_hash_table_lookup (hash_table: box->custom_slots, key: id); |
776 | |
777 | if (slot != parent) |
778 | return FALSE; |
779 | |
780 | gtk_widget_unparent (widget: child); |
781 | |
782 | return TRUE; |
783 | } |
784 | |