1 | /* gtkemojichooser.c: An Emoji chooser widget |
2 | * Copyright 2017, 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 | |
20 | #include "gtkemojichooser.h" |
21 | |
22 | #include "gtkadjustmentprivate.h" |
23 | #include "gtkbox.h" |
24 | #include "gtkbutton.h" |
25 | #include "gtkcssprovider.h" |
26 | #include "gtkentry.h" |
27 | #include "gtkflowboxprivate.h" |
28 | #include "gtkstack.h" |
29 | #include "gtklabel.h" |
30 | #include "gtkgesturelongpress.h" |
31 | #include "gtkpopover.h" |
32 | #include "gtkscrolledwindow.h" |
33 | #include "gtkintl.h" |
34 | #include "gtksearchentryprivate.h" |
35 | #include "gtktext.h" |
36 | #include "gtknative.h" |
37 | #include "gtkwidgetprivate.h" |
38 | #include "gdk/gdkprofilerprivate.h" |
39 | #include "gtkmain.h" |
40 | #include "gtkprivate.h" |
41 | |
42 | /** |
43 | * GtkEmojiChooser: |
44 | * |
45 | * The `GtkEmojiChooser` is used by text widgets such as `GtkEntry` or |
46 | * `GtkTextView` to let users insert Emoji characters. |
47 | * |
48 | * ![An example GtkEmojiChooser](emojichooser.png) |
49 | * |
50 | * `GtkEmojiChooser` emits the [signal@Gtk.EmojiChooser::emoji-picked] |
51 | * signal when an Emoji is selected. |
52 | * |
53 | * # CSS nodes |
54 | * |
55 | * ``` |
56 | * popover |
57 | * ├── box.emoji-searchbar |
58 | * │ ╰── entry.search |
59 | * ╰── box.emoji-toolbar |
60 | * ├── button.image-button.emoji-section |
61 | * ├── ... |
62 | * ╰── button.image-button.emoji-section |
63 | * ``` |
64 | * |
65 | * Every `GtkEmojiChooser` consists of a main node called popover. |
66 | * The contents of the popover are largely implementation defined |
67 | * and supposed to inherit general styles. |
68 | * The top searchbar used to search emoji and gets the .emoji-searchbar |
69 | * style class itself. |
70 | * The bottom toolbar used to switch between different emoji categories |
71 | * consists of buttons with the .emoji-section style class and gets the |
72 | * .emoji-toolbar style class itself. |
73 | */ |
74 | |
75 | #define BOX_SPACE 6 |
76 | |
77 | GType gtk_emoji_chooser_child_get_type (void); |
78 | |
79 | #define GTK_TYPE_EMOJI_CHOOSER_CHILD (gtk_emoji_chooser_child_get_type ()) |
80 | |
81 | typedef struct |
82 | { |
83 | GtkFlowBoxChild parent; |
84 | GtkWidget *variations; |
85 | } GtkEmojiChooserChild; |
86 | |
87 | typedef struct |
88 | { |
89 | GtkFlowBoxChildClass parent_class; |
90 | } GtkEmojiChooserChildClass; |
91 | |
92 | G_DEFINE_TYPE (GtkEmojiChooserChild, gtk_emoji_chooser_child, GTK_TYPE_FLOW_BOX_CHILD) |
93 | |
94 | static void |
95 | gtk_emoji_chooser_child_init (GtkEmojiChooserChild *child) |
96 | { |
97 | } |
98 | |
99 | static void |
100 | gtk_emoji_chooser_child_dispose (GObject *object) |
101 | { |
102 | GtkEmojiChooserChild *child = (GtkEmojiChooserChild *)object; |
103 | |
104 | g_clear_pointer (&child->variations, gtk_widget_unparent); |
105 | |
106 | G_OBJECT_CLASS (gtk_emoji_chooser_child_parent_class)->dispose (object); |
107 | } |
108 | |
109 | static void |
110 | gtk_emoji_chooser_child_size_allocate (GtkWidget *widget, |
111 | int width, |
112 | int height, |
113 | int baseline) |
114 | { |
115 | GtkEmojiChooserChild *child = (GtkEmojiChooserChild *)widget; |
116 | |
117 | GTK_WIDGET_CLASS (gtk_emoji_chooser_child_parent_class)->size_allocate (widget, width, height, baseline); |
118 | if (child->variations) |
119 | gtk_popover_present (GTK_POPOVER (child->variations)); |
120 | } |
121 | |
122 | static gboolean |
123 | gtk_emoji_chooser_child_focus (GtkWidget *widget, |
124 | GtkDirectionType direction) |
125 | { |
126 | GtkEmojiChooserChild *child = (GtkEmojiChooserChild *)widget; |
127 | |
128 | if (child->variations && gtk_widget_is_visible (widget: child->variations)) |
129 | { |
130 | if (gtk_widget_child_focus (widget: child->variations, direction)) |
131 | return TRUE; |
132 | } |
133 | |
134 | return GTK_WIDGET_CLASS (gtk_emoji_chooser_child_parent_class)->focus (widget, direction); |
135 | } |
136 | |
137 | static void scroll_to_child (GtkWidget *child); |
138 | |
139 | static gboolean |
140 | gtk_emoji_chooser_child_grab_focus (GtkWidget *widget) |
141 | { |
142 | gtk_widget_grab_focus_self (widget); |
143 | scroll_to_child (child: widget); |
144 | return TRUE; |
145 | } |
146 | |
147 | static void show_variations (GtkEmojiChooser *chooser, |
148 | GtkWidget *child); |
149 | |
150 | static void |
151 | (GtkWidget *widget, |
152 | const char *action_name, |
153 | GVariant *parameters) |
154 | { |
155 | GtkWidget *chooser; |
156 | |
157 | chooser = gtk_widget_get_ancestor (widget, GTK_TYPE_EMOJI_CHOOSER); |
158 | |
159 | show_variations (GTK_EMOJI_CHOOSER (chooser), child: widget); |
160 | } |
161 | |
162 | static void |
163 | gtk_emoji_chooser_child_class_init (GtkEmojiChooserChildClass *class) |
164 | { |
165 | GObjectClass *object_class = G_OBJECT_CLASS (class); |
166 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); |
167 | |
168 | object_class->dispose = gtk_emoji_chooser_child_dispose; |
169 | widget_class->size_allocate = gtk_emoji_chooser_child_size_allocate; |
170 | widget_class->focus = gtk_emoji_chooser_child_focus; |
171 | widget_class->grab_focus = gtk_emoji_chooser_child_grab_focus; |
172 | |
173 | gtk_widget_class_install_action (widget_class, action_name: "menu.popup" , NULL, activate: gtk_emoji_chooser_child_popup_menu); |
174 | |
175 | gtk_widget_class_add_binding_action (widget_class, |
176 | GDK_KEY_F10, mods: GDK_SHIFT_MASK, |
177 | action_name: "menu.popup" , |
178 | NULL); |
179 | gtk_widget_class_add_binding_action (widget_class, |
180 | GDK_KEY_Menu, mods: 0, |
181 | action_name: "menu.popup" , |
182 | NULL); |
183 | |
184 | gtk_widget_class_set_css_name (widget_class, name: "emoji" ); |
185 | } |
186 | |
187 | typedef struct { |
188 | GtkWidget *box; |
189 | GtkWidget *heading; |
190 | GtkWidget *button; |
191 | int group; |
192 | gunichar label; |
193 | gboolean empty; |
194 | } EmojiSection; |
195 | |
196 | struct _GtkEmojiChooser |
197 | { |
198 | GtkPopover parent_instance; |
199 | |
200 | GtkWidget *search_entry; |
201 | GtkWidget *stack; |
202 | GtkWidget *scrolled_window; |
203 | |
204 | int emoji_max_width; |
205 | |
206 | EmojiSection recent; |
207 | EmojiSection people; |
208 | EmojiSection body; |
209 | EmojiSection nature; |
210 | EmojiSection food; |
211 | EmojiSection travel; |
212 | EmojiSection activities; |
213 | EmojiSection objects; |
214 | EmojiSection symbols; |
215 | EmojiSection flags; |
216 | |
217 | GVariant *data; |
218 | GtkWidget *box; |
219 | GVariantIter *iter; |
220 | guint populate_idle; |
221 | |
222 | GSettings *settings; |
223 | }; |
224 | |
225 | struct _GtkEmojiChooserClass { |
226 | GtkPopoverClass parent_class; |
227 | }; |
228 | |
229 | enum { |
230 | EMOJI_PICKED, |
231 | LAST_SIGNAL |
232 | }; |
233 | |
234 | static int signals[LAST_SIGNAL]; |
235 | |
236 | G_DEFINE_TYPE (GtkEmojiChooser, gtk_emoji_chooser, GTK_TYPE_POPOVER) |
237 | |
238 | static void |
239 | gtk_emoji_chooser_finalize (GObject *object) |
240 | { |
241 | GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (object); |
242 | |
243 | if (chooser->populate_idle) |
244 | g_source_remove (tag: chooser->populate_idle); |
245 | |
246 | g_clear_pointer (&chooser->data, g_variant_unref); |
247 | g_clear_object (&chooser->settings); |
248 | |
249 | G_OBJECT_CLASS (gtk_emoji_chooser_parent_class)->finalize (object); |
250 | } |
251 | |
252 | static void |
253 | scroll_to_section (EmojiSection *section) |
254 | { |
255 | GtkEmojiChooser *chooser; |
256 | GtkAdjustment *adj; |
257 | GtkAllocation alloc = { 0, 0, 0, 0 }; |
258 | |
259 | chooser = GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (section->box, GTK_TYPE_EMOJI_CHOOSER)); |
260 | |
261 | adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); |
262 | if (section->heading) |
263 | gtk_widget_get_allocation (widget: section->heading, allocation: &alloc); |
264 | gtk_adjustment_animate_to_value (adjustment: adj, value: alloc.y - BOX_SPACE); |
265 | } |
266 | |
267 | static void |
268 | scroll_to_child (GtkWidget *child) |
269 | { |
270 | GtkEmojiChooser *chooser; |
271 | GtkAdjustment *adj; |
272 | GtkAllocation alloc; |
273 | double pos; |
274 | double value; |
275 | double page_size; |
276 | |
277 | chooser = GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (child, GTK_TYPE_EMOJI_CHOOSER)); |
278 | |
279 | adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); |
280 | |
281 | gtk_widget_get_allocation (widget: child, allocation: &alloc); |
282 | |
283 | value = gtk_adjustment_get_value (adjustment: adj); |
284 | page_size = gtk_adjustment_get_page_size (adjustment: adj); |
285 | |
286 | gtk_widget_translate_coordinates (src_widget: child, dest_widget: gtk_widget_get_parent (widget: chooser->recent.box), src_x: 0, src_y: 0, NULL, dest_y: &pos); |
287 | |
288 | if (pos < value) |
289 | gtk_adjustment_animate_to_value (adjustment: adj, value: pos); |
290 | else if (pos + alloc.height >= value + page_size) |
291 | gtk_adjustment_animate_to_value (adjustment: adj, value: value + ((pos + alloc.height) - (value + page_size))); |
292 | } |
293 | |
294 | static void |
295 | add_emoji (GtkWidget *box, |
296 | gboolean prepend, |
297 | GVariant *item, |
298 | gunichar modifier, |
299 | GtkEmojiChooser *chooser); |
300 | |
301 | #define MAX_RECENT (7*3) |
302 | |
303 | static void |
304 | populate_recent_section (GtkEmojiChooser *chooser) |
305 | { |
306 | GVariant *variant; |
307 | GVariant *item; |
308 | GVariantIter iter; |
309 | gboolean empty = FALSE; |
310 | |
311 | variant = g_settings_get_value (settings: chooser->settings, key: "recent-emoji" ); |
312 | g_variant_iter_init (iter: &iter, value: variant); |
313 | while ((item = g_variant_iter_next_value (iter: &iter))) |
314 | { |
315 | GVariant *emoji_data; |
316 | gunichar modifier; |
317 | |
318 | emoji_data = g_variant_get_child_value (value: item, index_: 0); |
319 | g_variant_get_child (value: item, index_: 1, format_string: "u" , &modifier); |
320 | add_emoji (box: chooser->recent.box, FALSE, item: emoji_data, modifier, chooser); |
321 | g_variant_unref (value: emoji_data); |
322 | g_variant_unref (value: item); |
323 | empty = FALSE; |
324 | } |
325 | |
326 | gtk_widget_set_visible (widget: chooser->recent.box, visible: !empty); |
327 | gtk_widget_set_sensitive (widget: chooser->recent.button, sensitive: !empty); |
328 | |
329 | g_variant_unref (value: variant); |
330 | } |
331 | |
332 | static void |
333 | add_recent_item (GtkEmojiChooser *chooser, |
334 | GVariant *item, |
335 | gunichar modifier) |
336 | { |
337 | GList *children, *l; |
338 | int i; |
339 | GVariantBuilder builder; |
340 | GtkWidget *child; |
341 | |
342 | g_variant_ref (value: item); |
343 | |
344 | g_variant_builder_init (builder: &builder, G_VARIANT_TYPE ("a((ausasu)u)" )); |
345 | g_variant_builder_add (builder: &builder, format_string: "(@(ausasu)u)" , item, modifier); |
346 | |
347 | children = NULL; |
348 | for (child = gtk_widget_get_last_child (widget: chooser->recent.box); |
349 | child != NULL; |
350 | child = gtk_widget_get_prev_sibling (widget: child)) |
351 | children = g_list_prepend (list: children, data: child); |
352 | |
353 | for (l = children, i = 1; l; l = l->next, i++) |
354 | { |
355 | GVariant *item2 = g_object_get_data (G_OBJECT (l->data), key: "emoji-data" ); |
356 | gunichar modifier2 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (l->data), "modifier" )); |
357 | |
358 | if (modifier == modifier2 && g_variant_equal (one: item, two: item2)) |
359 | { |
360 | gtk_flow_box_remove (GTK_FLOW_BOX (chooser->recent.box), widget: l->data); |
361 | i--; |
362 | continue; |
363 | } |
364 | if (i >= MAX_RECENT) |
365 | { |
366 | gtk_flow_box_remove (GTK_FLOW_BOX (chooser->recent.box), widget: l->data); |
367 | continue; |
368 | } |
369 | |
370 | g_variant_builder_add (builder: &builder, format_string: "(@(ausasu)u)" , item2, modifier2); |
371 | } |
372 | g_list_free (list: children); |
373 | |
374 | add_emoji (box: chooser->recent.box, TRUE, item, modifier, chooser); |
375 | |
376 | /* Enable recent */ |
377 | gtk_widget_show (widget: chooser->recent.box); |
378 | gtk_widget_set_sensitive (widget: chooser->recent.button, TRUE); |
379 | |
380 | g_settings_set_value (settings: chooser->settings, key: "recent-emoji" , value: g_variant_builder_end (builder: &builder)); |
381 | |
382 | g_variant_unref (value: item); |
383 | } |
384 | |
385 | static gboolean |
386 | should_close (GtkEmojiChooser *chooser) |
387 | { |
388 | GdkDisplay *display; |
389 | GdkSeat *seat; |
390 | GdkDevice *device; |
391 | GdkModifierType state; |
392 | |
393 | display = gtk_widget_get_display (GTK_WIDGET (chooser)); |
394 | seat = gdk_display_get_default_seat (display); |
395 | device = gdk_seat_get_keyboard (seat); |
396 | state = gdk_device_get_modifier_state (device); |
397 | |
398 | return (state & GDK_CONTROL_MASK) == 0; |
399 | } |
400 | |
401 | static void |
402 | emoji_activated (GtkFlowBox *box, |
403 | GtkFlowBoxChild *child, |
404 | gpointer data) |
405 | { |
406 | GtkEmojiChooser *chooser = data; |
407 | char *text; |
408 | GtkWidget *label; |
409 | GVariant *item; |
410 | gunichar modifier; |
411 | |
412 | if (should_close (chooser)) |
413 | gtk_popover_popdown (GTK_POPOVER (chooser)); |
414 | else |
415 | { |
416 | GtkWidget *popover; |
417 | |
418 | popover = gtk_widget_get_ancestor (GTK_WIDGET (box), GTK_TYPE_POPOVER); |
419 | if (popover != GTK_WIDGET (chooser)) |
420 | gtk_popover_popdown (GTK_POPOVER (popover)); |
421 | } |
422 | |
423 | label = gtk_flow_box_child_get_child (self: child); |
424 | text = g_strdup (str: gtk_label_get_label (GTK_LABEL (label))); |
425 | |
426 | item = (GVariant*) g_object_get_data (G_OBJECT (child), key: "emoji-data" ); |
427 | modifier = (gunichar) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (child), "modifier" )); |
428 | add_recent_item (chooser, item, modifier); |
429 | |
430 | g_signal_emit (instance: data, signal_id: signals[EMOJI_PICKED], detail: 0, text); |
431 | g_free (mem: text); |
432 | } |
433 | |
434 | static gboolean |
435 | has_variations (GVariant *emoji_data) |
436 | { |
437 | GVariant *codes; |
438 | int i; |
439 | gboolean has_variations; |
440 | |
441 | has_variations = FALSE; |
442 | codes = g_variant_get_child_value (value: emoji_data, index_: 0); |
443 | for (i = 0; i < g_variant_n_children (value: codes); i++) |
444 | { |
445 | gunichar code; |
446 | g_variant_get_child (value: codes, index_: i, format_string: "u" , &code); |
447 | if (code == 0) |
448 | { |
449 | has_variations = TRUE; |
450 | break; |
451 | } |
452 | } |
453 | g_variant_unref (value: codes); |
454 | |
455 | return has_variations; |
456 | } |
457 | |
458 | static void |
459 | show_variations (GtkEmojiChooser *chooser, |
460 | GtkWidget *child) |
461 | { |
462 | GtkWidget *popover; |
463 | GtkWidget *view; |
464 | GtkWidget *box; |
465 | GVariant *emoji_data; |
466 | GtkWidget *parent_popover; |
467 | gunichar modifier; |
468 | GtkEmojiChooserChild *ch = (GtkEmojiChooserChild *)child; |
469 | |
470 | if (!child) |
471 | return; |
472 | |
473 | emoji_data = (GVariant*) g_object_get_data (G_OBJECT (child), key: "emoji-data" ); |
474 | if (!emoji_data) |
475 | return; |
476 | |
477 | if (!has_variations (emoji_data)) |
478 | return; |
479 | |
480 | parent_popover = gtk_widget_get_ancestor (widget: child, GTK_TYPE_POPOVER); |
481 | g_clear_pointer (&ch->variations, gtk_widget_unparent); |
482 | popover = ch->variations = gtk_popover_new (); |
483 | gtk_popover_set_autohide (GTK_POPOVER (popover), TRUE); |
484 | gtk_widget_set_parent (widget: popover, parent: child); |
485 | view = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 0); |
486 | gtk_widget_add_css_class (widget: view, css_class: "view" ); |
487 | box = gtk_flow_box_new (); |
488 | gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (box), TRUE); |
489 | gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (box), n_children: 6); |
490 | gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (box), n_children: 6); |
491 | gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (box), TRUE); |
492 | gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (box), mode: GTK_SELECTION_NONE); |
493 | g_object_set (object: box, first_property_name: "accept-unpaired-release" , TRUE, NULL); |
494 | gtk_popover_set_child (GTK_POPOVER (popover), child: view); |
495 | gtk_box_append (GTK_BOX (view), child: box); |
496 | |
497 | g_signal_connect (box, "child-activated" , G_CALLBACK (emoji_activated), parent_popover); |
498 | |
499 | add_emoji (box, FALSE, item: emoji_data, modifier: 0, chooser); |
500 | for (modifier = 0x1f3fb; modifier <= 0x1f3ff; modifier++) |
501 | add_emoji (box, FALSE, item: emoji_data, modifier, chooser); |
502 | |
503 | gtk_popover_popup (GTK_POPOVER (popover)); |
504 | } |
505 | |
506 | static void |
507 | long_pressed_cb (GtkGesture *gesture, |
508 | double x, |
509 | double y, |
510 | gpointer data) |
511 | { |
512 | GtkEmojiChooser *chooser = data; |
513 | GtkWidget *box; |
514 | GtkWidget *child; |
515 | |
516 | box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); |
517 | child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y)); |
518 | show_variations (chooser, child); |
519 | } |
520 | |
521 | static void |
522 | pressed_cb (GtkGesture *gesture, |
523 | int n_press, |
524 | double x, |
525 | double y, |
526 | gpointer data) |
527 | { |
528 | GtkEmojiChooser *chooser = data; |
529 | GtkWidget *box; |
530 | GtkWidget *child; |
531 | |
532 | box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); |
533 | child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y)); |
534 | show_variations (chooser, child); |
535 | } |
536 | |
537 | static void |
538 | add_emoji (GtkWidget *box, |
539 | gboolean prepend, |
540 | GVariant *item, |
541 | gunichar modifier, |
542 | GtkEmojiChooser *chooser) |
543 | { |
544 | GtkWidget *child; |
545 | GtkWidget *label; |
546 | PangoAttrList *attrs; |
547 | GVariant *codes; |
548 | char text[64]; |
549 | char *p = text; |
550 | int i; |
551 | PangoLayout *layout; |
552 | PangoRectangle rect; |
553 | |
554 | codes = g_variant_get_child_value (value: item, index_: 0); |
555 | for (i = 0; i < g_variant_n_children (value: codes); i++) |
556 | { |
557 | gunichar code; |
558 | |
559 | g_variant_get_child (value: codes, index_: i, format_string: "u" , &code); |
560 | if (code == 0) |
561 | code = modifier; |
562 | if (code != 0) |
563 | p += g_unichar_to_utf8 (c: code, outbuf: p); |
564 | } |
565 | g_variant_unref (value: codes); |
566 | p += g_unichar_to_utf8 (c: 0xFE0F, outbuf: p); /* U+FE0F is the Emoji variation selector */ |
567 | p[0] = 0; |
568 | |
569 | label = gtk_label_new (str: text); |
570 | attrs = pango_attr_list_new (); |
571 | pango_attr_list_insert (list: attrs, attr: pango_attr_scale_new (PANGO_SCALE_X_LARGE)); |
572 | gtk_label_set_attributes (GTK_LABEL (label), attrs); |
573 | pango_attr_list_unref (list: attrs); |
574 | |
575 | layout = gtk_label_get_layout (GTK_LABEL (label)); |
576 | pango_layout_get_extents (layout, ink_rect: &rect, NULL); |
577 | |
578 | /* Check for fallback rendering that generates too wide items */ |
579 | if (pango_layout_get_unknown_glyphs_count (layout) > 0 || |
580 | rect.width >= 1.5 * chooser->emoji_max_width) |
581 | { |
582 | g_object_ref_sink (label); |
583 | g_object_unref (object: label); |
584 | return; |
585 | } |
586 | |
587 | child = g_object_new (GTK_TYPE_EMOJI_CHOOSER_CHILD, NULL); |
588 | g_object_set_data_full (G_OBJECT (child), key: "emoji-data" , |
589 | data: g_variant_ref (value: item), |
590 | destroy: (GDestroyNotify)g_variant_unref); |
591 | if (modifier != 0) |
592 | g_object_set_data (G_OBJECT (child), key: "modifier" , GUINT_TO_POINTER (modifier)); |
593 | |
594 | gtk_flow_box_child_set_child (GTK_FLOW_BOX_CHILD (child), child: label); |
595 | gtk_flow_box_insert (GTK_FLOW_BOX (box), widget: child, position: prepend ? 0 : -1); |
596 | } |
597 | |
598 | static GBytes * |
599 | get_emoji_data_by_language (const char *lang) |
600 | { |
601 | GBytes *bytes; |
602 | char *path; |
603 | GError *error = NULL; |
604 | |
605 | path = g_strconcat (string1: "/org/gtk/libgtk/emoji/" , lang, ".data" , NULL); |
606 | bytes = g_resources_lookup_data (path, lookup_flags: 0, error: &error); |
607 | if (bytes) |
608 | { |
609 | g_debug ("Found emoji data for %s in resource %s" , lang, path); |
610 | g_free (mem: path); |
611 | return bytes; |
612 | } |
613 | |
614 | if (g_error_matches (error, G_RESOURCE_ERROR, code: G_RESOURCE_ERROR_NOT_FOUND)) |
615 | { |
616 | char *filename; |
617 | char *gresource_name; |
618 | GMappedFile *file; |
619 | |
620 | g_clear_error (err: &error); |
621 | |
622 | gresource_name = g_strconcat (string1: lang, ".gresource" , NULL); |
623 | filename = g_build_filename (first_element: _gtk_get_data_prefix (), "share" , "gtk-4.0" , |
624 | "emoji" , gresource_name, NULL); |
625 | g_clear_pointer (&gresource_name, g_free); |
626 | file = g_mapped_file_new (filename, FALSE, NULL); |
627 | |
628 | if (file) |
629 | { |
630 | GBytes *data; |
631 | GResource *resource; |
632 | |
633 | data = g_mapped_file_get_bytes (file); |
634 | g_mapped_file_unref (file); |
635 | |
636 | resource = g_resource_new_from_data (data, NULL); |
637 | g_bytes_unref (bytes: data); |
638 | |
639 | g_debug ("Registering resource for Emoji data for %s from file %s" , lang, filename); |
640 | g_resources_register (resource); |
641 | g_resource_unref (resource); |
642 | |
643 | bytes = g_resources_lookup_data (path, lookup_flags: 0, NULL); |
644 | if (bytes) |
645 | { |
646 | g_debug ("Found emoji data for %s in resource %s" , lang, path); |
647 | g_free (mem: path); |
648 | g_free (mem: filename); |
649 | return bytes; |
650 | } |
651 | } |
652 | |
653 | g_free (mem: filename); |
654 | } |
655 | |
656 | g_clear_error (err: &error); |
657 | g_free (mem: path); |
658 | |
659 | return NULL; |
660 | } |
661 | |
662 | GBytes * |
663 | get_emoji_data (void) |
664 | { |
665 | GBytes *bytes; |
666 | const char *lang; |
667 | |
668 | lang = pango_language_to_string (gtk_get_default_language ()); |
669 | bytes = get_emoji_data_by_language (lang); |
670 | if (bytes) |
671 | return bytes; |
672 | |
673 | if (strchr (s: lang, c: '-')) |
674 | { |
675 | char q[5]; |
676 | int i; |
677 | |
678 | for (i = 0; lang[i] != '-' && i < 4; i++) |
679 | q[i] = lang[i]; |
680 | q[i] = '\0'; |
681 | |
682 | bytes = get_emoji_data_by_language (lang: q); |
683 | if (bytes) |
684 | return bytes; |
685 | } |
686 | |
687 | bytes = get_emoji_data_by_language (lang: "en" ); |
688 | g_assert (bytes); |
689 | |
690 | return bytes; |
691 | } |
692 | |
693 | static gboolean |
694 | populate_emoji_chooser (gpointer data) |
695 | { |
696 | GtkEmojiChooser *chooser = data; |
697 | GVariant *item; |
698 | gint64 start, now; |
699 | |
700 | start = g_get_monotonic_time (); |
701 | |
702 | if (!chooser->data) |
703 | { |
704 | GBytes *bytes; |
705 | |
706 | bytes = get_emoji_data (); |
707 | |
708 | chooser->data = g_variant_ref_sink (value: g_variant_new_from_bytes (G_VARIANT_TYPE ("a(ausasu)" ), bytes, TRUE)); |
709 | g_bytes_unref (bytes); |
710 | } |
711 | |
712 | if (!chooser->iter) |
713 | { |
714 | chooser->iter = g_variant_iter_new (value: chooser->data); |
715 | chooser->box = chooser->people.box; |
716 | } |
717 | |
718 | while ((item = g_variant_iter_next_value (iter: chooser->iter))) |
719 | { |
720 | guint group; |
721 | |
722 | g_variant_get_child (value: item, index_: 3, format_string: "u" , &group); |
723 | |
724 | if (group == chooser->people.group) |
725 | chooser->box = chooser->people.box; |
726 | else if (group == chooser->body.group) |
727 | chooser->box = chooser->body.box; |
728 | else if (group == chooser->nature.group) |
729 | chooser->box = chooser->nature.box; |
730 | else if (group == chooser->food.group) |
731 | chooser->box = chooser->food.box; |
732 | else if (group == chooser->travel.group) |
733 | chooser->box = chooser->travel.box; |
734 | else if (group == chooser->activities.group) |
735 | chooser->box = chooser->activities.box; |
736 | else if (group == chooser->objects.group) |
737 | chooser->box = chooser->objects.box; |
738 | else if (group == chooser->symbols.group) |
739 | chooser->box = chooser->symbols.box; |
740 | else if (group == chooser->flags.group) |
741 | chooser->box = chooser->flags.box; |
742 | |
743 | add_emoji (box: chooser->box, FALSE, item, modifier: 0, chooser); |
744 | g_variant_unref (value: item); |
745 | |
746 | now = g_get_monotonic_time (); |
747 | if (now > start + 200) /* 2 ms */ |
748 | { |
749 | gdk_profiler_add_mark (start * 1000, (now - start) * 1000, "emojichooser" , "populate" ); |
750 | return G_SOURCE_CONTINUE; |
751 | } |
752 | } |
753 | |
754 | g_variant_iter_free (iter: chooser->iter); |
755 | chooser->iter = NULL; |
756 | chooser->box = NULL; |
757 | chooser->populate_idle = 0; |
758 | |
759 | gdk_profiler_end_mark (start, "emojichooser" , "populate (finish)" ); |
760 | |
761 | return G_SOURCE_REMOVE; |
762 | } |
763 | |
764 | static void |
765 | adj_value_changed (GtkAdjustment *adj, |
766 | gpointer data) |
767 | { |
768 | GtkEmojiChooser *chooser = data; |
769 | double value = gtk_adjustment_get_value (adjustment: adj); |
770 | EmojiSection const *sections[] = { |
771 | &chooser->recent, |
772 | &chooser->people, |
773 | &chooser->body, |
774 | &chooser->nature, |
775 | &chooser->food, |
776 | &chooser->travel, |
777 | &chooser->activities, |
778 | &chooser->objects, |
779 | &chooser->symbols, |
780 | &chooser->flags, |
781 | }; |
782 | EmojiSection const *select_section = sections[0]; |
783 | gsize i; |
784 | |
785 | /* Figure out which section the current scroll position is within */ |
786 | for (i = 0; i < G_N_ELEMENTS (sections); ++i) |
787 | { |
788 | EmojiSection const *section = sections[i]; |
789 | GtkAllocation alloc; |
790 | |
791 | if (!gtk_widget_get_visible (widget: section->box)) |
792 | continue; |
793 | |
794 | if (section->heading) |
795 | gtk_widget_get_allocation (widget: section->heading, allocation: &alloc); |
796 | else |
797 | gtk_widget_get_allocation (widget: section->box, allocation: &alloc); |
798 | |
799 | if (value < alloc.y - BOX_SPACE) |
800 | break; |
801 | |
802 | select_section = section; |
803 | } |
804 | |
805 | /* Un/Check the section buttons accordingly */ |
806 | for (i = 0; i < G_N_ELEMENTS (sections); ++i) |
807 | { |
808 | EmojiSection const *section = sections[i]; |
809 | |
810 | if (section == select_section) |
811 | gtk_widget_set_state_flags (widget: section->button, flags: GTK_STATE_FLAG_CHECKED, FALSE); |
812 | else |
813 | gtk_widget_unset_state_flags (widget: section->button, flags: GTK_STATE_FLAG_CHECKED); |
814 | } |
815 | } |
816 | |
817 | static gboolean |
818 | match_tokens (const char **term_tokens, |
819 | const char **hit_tokens) |
820 | { |
821 | int i, j; |
822 | gboolean matched; |
823 | |
824 | matched = TRUE; |
825 | |
826 | for (i = 0; term_tokens[i]; i++) |
827 | { |
828 | for (j = 0; hit_tokens[j]; j++) |
829 | if (g_str_has_prefix (str: hit_tokens[j], prefix: term_tokens[i])) |
830 | goto one_matched; |
831 | |
832 | matched = FALSE; |
833 | break; |
834 | |
835 | one_matched: |
836 | continue; |
837 | } |
838 | |
839 | return matched; |
840 | } |
841 | |
842 | static gboolean |
843 | filter_func (GtkFlowBoxChild *child, |
844 | gpointer data) |
845 | { |
846 | EmojiSection *section = data; |
847 | GtkEmojiChooser *chooser; |
848 | GVariant *emoji_data; |
849 | const char *text; |
850 | const char *name; |
851 | const char **keywords; |
852 | char **term_tokens; |
853 | char **name_tokens; |
854 | gboolean res; |
855 | |
856 | res = TRUE; |
857 | |
858 | chooser = GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), GTK_TYPE_EMOJI_CHOOSER)); |
859 | text = gtk_editable_get_text (GTK_EDITABLE (chooser->search_entry)); |
860 | emoji_data = (GVariant *) g_object_get_data (G_OBJECT (child), key: "emoji-data" ); |
861 | |
862 | if (text[0] == 0) |
863 | goto out; |
864 | |
865 | if (!emoji_data) |
866 | goto out; |
867 | |
868 | term_tokens = g_str_tokenize_and_fold (string: text, translit_locale: "en" , NULL); |
869 | |
870 | g_variant_get_child (value: emoji_data, index_: 1, format_string: "&s" , &name); |
871 | name_tokens = g_str_tokenize_and_fold (string: name, translit_locale: "en" , NULL); |
872 | g_variant_get_child (value: emoji_data, index_: 2, format_string: "^a&s" , &keywords); |
873 | |
874 | res = match_tokens (term_tokens: (const char **)term_tokens, hit_tokens: (const char **)name_tokens) || |
875 | match_tokens (term_tokens: (const char **)term_tokens, hit_tokens: keywords); |
876 | |
877 | g_strfreev (str_array: term_tokens); |
878 | g_strfreev (str_array: name_tokens); |
879 | |
880 | out: |
881 | if (res) |
882 | section->empty = FALSE; |
883 | |
884 | return res; |
885 | } |
886 | |
887 | static void |
888 | invalidate_section (EmojiSection *section) |
889 | { |
890 | section->empty = TRUE; |
891 | gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (section->box)); |
892 | } |
893 | |
894 | static void |
895 | update_headings (GtkEmojiChooser *chooser) |
896 | { |
897 | gtk_widget_set_visible (widget: chooser->people.heading, visible: !chooser->people.empty); |
898 | gtk_widget_set_visible (widget: chooser->people.box, visible: !chooser->people.empty); |
899 | gtk_widget_set_visible (widget: chooser->body.heading, visible: !chooser->body.empty); |
900 | gtk_widget_set_visible (widget: chooser->body.box, visible: !chooser->body.empty); |
901 | gtk_widget_set_visible (widget: chooser->nature.heading, visible: !chooser->nature.empty); |
902 | gtk_widget_set_visible (widget: chooser->nature.box, visible: !chooser->nature.empty); |
903 | gtk_widget_set_visible (widget: chooser->food.heading, visible: !chooser->food.empty); |
904 | gtk_widget_set_visible (widget: chooser->food.box, visible: !chooser->food.empty); |
905 | gtk_widget_set_visible (widget: chooser->travel.heading, visible: !chooser->travel.empty); |
906 | gtk_widget_set_visible (widget: chooser->travel.box, visible: !chooser->travel.empty); |
907 | gtk_widget_set_visible (widget: chooser->activities.heading, visible: !chooser->activities.empty); |
908 | gtk_widget_set_visible (widget: chooser->activities.box, visible: !chooser->activities.empty); |
909 | gtk_widget_set_visible (widget: chooser->objects.heading, visible: !chooser->objects.empty); |
910 | gtk_widget_set_visible (widget: chooser->objects.box, visible: !chooser->objects.empty); |
911 | gtk_widget_set_visible (widget: chooser->symbols.heading, visible: !chooser->symbols.empty); |
912 | gtk_widget_set_visible (widget: chooser->symbols.box, visible: !chooser->symbols.empty); |
913 | gtk_widget_set_visible (widget: chooser->flags.heading, visible: !chooser->flags.empty); |
914 | gtk_widget_set_visible (widget: chooser->flags.box, visible: !chooser->flags.empty); |
915 | |
916 | if (chooser->recent.empty && chooser->people.empty && |
917 | chooser->body.empty && chooser->nature.empty && |
918 | chooser->food.empty && chooser->travel.empty && |
919 | chooser->activities.empty && chooser->objects.empty && |
920 | chooser->symbols.empty && chooser->flags.empty) |
921 | gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), name: "empty" ); |
922 | else |
923 | gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), name: "list" ); |
924 | } |
925 | |
926 | static void |
927 | search_changed (GtkEntry *entry, |
928 | gpointer data) |
929 | { |
930 | GtkEmojiChooser *chooser = data; |
931 | |
932 | invalidate_section (section: &chooser->recent); |
933 | invalidate_section (section: &chooser->people); |
934 | invalidate_section (section: &chooser->body); |
935 | invalidate_section (section: &chooser->nature); |
936 | invalidate_section (section: &chooser->food); |
937 | invalidate_section (section: &chooser->travel); |
938 | invalidate_section (section: &chooser->activities); |
939 | invalidate_section (section: &chooser->objects); |
940 | invalidate_section (section: &chooser->symbols); |
941 | invalidate_section (section: &chooser->flags); |
942 | |
943 | update_headings (chooser); |
944 | } |
945 | |
946 | static void |
947 | stop_search (GtkEntry *entry, |
948 | gpointer data) |
949 | { |
950 | gtk_popover_popdown (GTK_POPOVER (data)); |
951 | } |
952 | |
953 | static void |
954 | setup_section (GtkEmojiChooser *chooser, |
955 | EmojiSection *section, |
956 | int group, |
957 | const char *icon) |
958 | { |
959 | section->group = group; |
960 | |
961 | gtk_button_set_icon_name (GTK_BUTTON (section->button), icon_name: icon); |
962 | |
963 | gtk_flow_box_disable_move_cursor (GTK_FLOW_BOX (section->box)); |
964 | gtk_flow_box_set_filter_func (GTK_FLOW_BOX (section->box), filter_func, user_data: section, NULL); |
965 | g_signal_connect_swapped (section->button, "clicked" , G_CALLBACK (scroll_to_section), section); |
966 | } |
967 | |
968 | static void |
969 | gtk_emoji_chooser_init (GtkEmojiChooser *chooser) |
970 | { |
971 | GtkAdjustment *adj; |
972 | GtkText *text; |
973 | |
974 | chooser->settings = g_settings_new (schema_id: "org.gtk.gtk4.Settings.EmojiChooser" ); |
975 | |
976 | gtk_widget_init_template (GTK_WIDGET (chooser)); |
977 | |
978 | text = gtk_search_entry_get_text_widget (GTK_SEARCH_ENTRY (chooser->search_entry)); |
979 | gtk_text_set_input_hints (self: text, hints: GTK_INPUT_HINT_NO_EMOJI); |
980 | |
981 | /* Get a reasonable maximum width for an emoji. We do this to |
982 | * skip overly wide fallback rendering for certain emojis the |
983 | * font does not contain and therefore end up being rendered |
984 | * as multiply glyphs. |
985 | */ |
986 | { |
987 | PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET (chooser), text: "🙂" ); |
988 | PangoAttrList *attrs; |
989 | PangoRectangle rect; |
990 | |
991 | attrs = pango_attr_list_new (); |
992 | pango_attr_list_insert (list: attrs, attr: pango_attr_scale_new (PANGO_SCALE_X_LARGE)); |
993 | pango_layout_set_attributes (layout, attrs); |
994 | pango_attr_list_unref (list: attrs); |
995 | |
996 | pango_layout_get_extents (layout, ink_rect: &rect, NULL); |
997 | chooser->emoji_max_width = rect.width; |
998 | |
999 | g_object_unref (object: layout); |
1000 | } |
1001 | |
1002 | adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); |
1003 | g_signal_connect (adj, "value-changed" , G_CALLBACK (adj_value_changed), chooser); |
1004 | |
1005 | setup_section (chooser, section: &chooser->recent, group: -1, icon: "emoji-recent-symbolic" ); |
1006 | setup_section (chooser, section: &chooser->people, group: 0, icon: "emoji-people-symbolic" ); |
1007 | setup_section (chooser, section: &chooser->body, group: 1, icon: "emoji-body-symbolic" ); |
1008 | setup_section (chooser, section: &chooser->nature, group: 3, icon: "emoji-nature-symbolic" ); |
1009 | setup_section (chooser, section: &chooser->food, group: 4, icon: "emoji-food-symbolic" ); |
1010 | setup_section (chooser, section: &chooser->travel, group: 5, icon: "emoji-travel-symbolic" ); |
1011 | setup_section (chooser, section: &chooser->activities, group: 6, icon: "emoji-activities-symbolic" ); |
1012 | setup_section (chooser, section: &chooser->objects, group: 7, icon: "emoji-objects-symbolic" ); |
1013 | setup_section (chooser, section: &chooser->symbols, group: 8, icon: "emoji-symbols-symbolic" ); |
1014 | setup_section (chooser, section: &chooser->flags, group: 9, icon: "emoji-flags-symbolic" ); |
1015 | |
1016 | populate_recent_section (chooser); |
1017 | |
1018 | chooser->populate_idle = g_idle_add (function: populate_emoji_chooser, data: chooser); |
1019 | gdk_source_set_static_name_by_id (tag: chooser->populate_idle, name: "[gtk] populate_emoji_chooser" ); |
1020 | } |
1021 | |
1022 | static void |
1023 | gtk_emoji_chooser_show (GtkWidget *widget) |
1024 | { |
1025 | GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (widget); |
1026 | GtkAdjustment *adj; |
1027 | |
1028 | GTK_WIDGET_CLASS (gtk_emoji_chooser_parent_class)->show (widget); |
1029 | |
1030 | adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); |
1031 | gtk_adjustment_set_value (adjustment: adj, value: 0); |
1032 | adj_value_changed (adj, data: chooser); |
1033 | |
1034 | gtk_editable_set_text (GTK_EDITABLE (chooser->search_entry), text: "" ); |
1035 | } |
1036 | |
1037 | static EmojiSection * |
1038 | find_section (GtkEmojiChooser *chooser, |
1039 | GtkWidget *box) |
1040 | { |
1041 | if (box == chooser->recent.box) |
1042 | return &chooser->recent; |
1043 | else if (box == chooser->people.box) |
1044 | return &chooser->people; |
1045 | else if (box == chooser->body.box) |
1046 | return &chooser->body; |
1047 | else if (box == chooser->nature.box) |
1048 | return &chooser->nature; |
1049 | else if (box == chooser->food.box) |
1050 | return &chooser->food; |
1051 | else if (box == chooser->travel.box) |
1052 | return &chooser->travel; |
1053 | else if (box == chooser->activities.box) |
1054 | return &chooser->activities; |
1055 | else if (box == chooser->objects.box) |
1056 | return &chooser->objects; |
1057 | else if (box == chooser->symbols.box) |
1058 | return &chooser->symbols; |
1059 | else if (box == chooser->flags.box) |
1060 | return &chooser->flags; |
1061 | else |
1062 | return NULL; |
1063 | } |
1064 | |
1065 | static EmojiSection * |
1066 | find_next_section (GtkEmojiChooser *chooser, |
1067 | GtkWidget *box, |
1068 | gboolean down) |
1069 | { |
1070 | EmojiSection *next; |
1071 | |
1072 | if (box == chooser->recent.box) |
1073 | next = down ? &chooser->people : NULL; |
1074 | else if (box == chooser->people.box) |
1075 | next = down ? &chooser->body : &chooser->recent; |
1076 | else if (box == chooser->body.box) |
1077 | next = down ? &chooser->nature : &chooser->people; |
1078 | else if (box == chooser->nature.box) |
1079 | next = down ? &chooser->food : &chooser->body; |
1080 | else if (box == chooser->food.box) |
1081 | next = down ? &chooser->travel : &chooser->nature; |
1082 | else if (box == chooser->travel.box) |
1083 | next = down ? &chooser->activities : &chooser->food; |
1084 | else if (box == chooser->activities.box) |
1085 | next = down ? &chooser->objects : &chooser->travel; |
1086 | else if (box == chooser->objects.box) |
1087 | next = down ? &chooser->symbols : &chooser->activities; |
1088 | else if (box == chooser->symbols.box) |
1089 | next = down ? &chooser->flags : &chooser->objects; |
1090 | else if (box == chooser->flags.box) |
1091 | next = down ? NULL : &chooser->symbols; |
1092 | else |
1093 | next = NULL; |
1094 | |
1095 | return next; |
1096 | } |
1097 | |
1098 | static void |
1099 | gtk_emoji_chooser_scroll_section (GtkWidget *widget, |
1100 | const char *action_name, |
1101 | GVariant *parameter) |
1102 | { |
1103 | GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (widget); |
1104 | int direction = g_variant_get_int32 (value: parameter); |
1105 | GtkWidget *focus; |
1106 | GtkWidget *box; |
1107 | EmojiSection *next; |
1108 | |
1109 | focus = gtk_root_get_focus (self: gtk_widget_get_root (widget)); |
1110 | if (focus == NULL) |
1111 | return; |
1112 | |
1113 | if (gtk_widget_is_ancestor (widget: focus, ancestor: chooser->search_entry)) |
1114 | box = chooser->recent.box; |
1115 | else |
1116 | box = gtk_widget_get_ancestor (widget: focus, GTK_TYPE_FLOW_BOX); |
1117 | |
1118 | next = find_next_section (chooser, box, down: direction > 0); |
1119 | |
1120 | if (next) |
1121 | { |
1122 | gtk_widget_child_focus (widget: next->box, direction: GTK_DIR_TAB_FORWARD); |
1123 | scroll_to_section (section: next); |
1124 | } |
1125 | } |
1126 | |
1127 | static gboolean |
1128 | keynav_failed (GtkWidget *box, |
1129 | GtkDirectionType direction, |
1130 | GtkEmojiChooser *chooser) |
1131 | { |
1132 | EmojiSection *next; |
1133 | GtkWidget *focus; |
1134 | GtkWidget *child; |
1135 | GtkWidget *sibling; |
1136 | GtkAllocation alloc; |
1137 | int i; |
1138 | int column; |
1139 | int child_x; |
1140 | |
1141 | focus = gtk_root_get_focus (self: gtk_widget_get_root (widget: box)); |
1142 | if (focus == NULL) |
1143 | return FALSE; |
1144 | |
1145 | child = gtk_widget_get_ancestor (widget: focus, GTK_TYPE_EMOJI_CHOOSER_CHILD); |
1146 | |
1147 | column = 0; |
1148 | child_x = G_MAXINT; |
1149 | for (sibling = gtk_widget_get_first_child (widget: box); |
1150 | sibling; |
1151 | sibling = gtk_widget_get_next_sibling (widget: sibling)) |
1152 | { |
1153 | if (!gtk_widget_get_child_visible (widget: sibling)) |
1154 | continue; |
1155 | |
1156 | gtk_widget_get_allocation (widget: sibling, allocation: &alloc); |
1157 | |
1158 | if (alloc.x < child_x) |
1159 | column = 0; |
1160 | else |
1161 | column++; |
1162 | |
1163 | child_x = alloc.x; |
1164 | |
1165 | if (sibling == child) |
1166 | break; |
1167 | } |
1168 | |
1169 | if (direction == GTK_DIR_DOWN) |
1170 | { |
1171 | next = find_section (chooser, box); |
1172 | while (TRUE) |
1173 | { |
1174 | next = find_next_section (chooser, box: next->box, TRUE); |
1175 | if (next == NULL) |
1176 | return FALSE; |
1177 | |
1178 | i = 0; |
1179 | child_x = G_MAXINT; |
1180 | for (sibling = gtk_widget_get_first_child (widget: next->box); |
1181 | sibling; |
1182 | sibling = gtk_widget_get_next_sibling (widget: sibling)) |
1183 | { |
1184 | if (!gtk_widget_get_child_visible (widget: sibling)) |
1185 | continue; |
1186 | |
1187 | gtk_widget_get_allocation (widget: sibling, allocation: &alloc); |
1188 | |
1189 | if (alloc.x < child_x) |
1190 | i = 0; |
1191 | else |
1192 | i++; |
1193 | |
1194 | child_x = alloc.x; |
1195 | |
1196 | if (i == column) |
1197 | { |
1198 | gtk_widget_grab_focus (widget: sibling); |
1199 | return TRUE; |
1200 | } |
1201 | } |
1202 | } |
1203 | } |
1204 | else if (direction == GTK_DIR_UP) |
1205 | { |
1206 | next = find_section (chooser, box); |
1207 | while (TRUE) |
1208 | { |
1209 | next = find_next_section (chooser, box: next->box, FALSE); |
1210 | if (next == NULL) |
1211 | return FALSE; |
1212 | |
1213 | i = 0; |
1214 | child_x = G_MAXINT; |
1215 | child = NULL; |
1216 | for (sibling = gtk_widget_get_first_child (widget: next->box); |
1217 | sibling; |
1218 | sibling = gtk_widget_get_next_sibling (widget: sibling)) |
1219 | { |
1220 | if (!gtk_widget_get_child_visible (widget: sibling)) |
1221 | continue; |
1222 | |
1223 | gtk_widget_get_allocation (widget: sibling, allocation: &alloc); |
1224 | |
1225 | if (alloc.x < child_x) |
1226 | i = 0; |
1227 | else |
1228 | i++; |
1229 | |
1230 | child_x = alloc.x; |
1231 | |
1232 | if (i == column) |
1233 | child = sibling; |
1234 | } |
1235 | |
1236 | if (child) |
1237 | { |
1238 | gtk_widget_grab_focus (widget: child); |
1239 | return TRUE; |
1240 | } |
1241 | } |
1242 | } |
1243 | |
1244 | return FALSE; |
1245 | } |
1246 | |
1247 | static void |
1248 | gtk_emoji_chooser_map (GtkWidget *widget) |
1249 | { |
1250 | GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (widget); |
1251 | |
1252 | GTK_WIDGET_CLASS (gtk_emoji_chooser_parent_class)->map (widget); |
1253 | |
1254 | gtk_widget_grab_focus (widget: chooser->search_entry); |
1255 | } |
1256 | |
1257 | static void |
1258 | gtk_emoji_chooser_class_init (GtkEmojiChooserClass *klass) |
1259 | { |
1260 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
1261 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
1262 | |
1263 | object_class->finalize = gtk_emoji_chooser_finalize; |
1264 | widget_class->show = gtk_emoji_chooser_show; |
1265 | widget_class->map = gtk_emoji_chooser_map; |
1266 | |
1267 | /** |
1268 | * GtkEmojiChooser::emoji-picked: |
1269 | * @chooser: the `GtkEmojiChooser` |
1270 | * @text: the Unicode sequence for the picked Emoji, in UTF-8 |
1271 | * |
1272 | * Emitted when the user selects an Emoji. |
1273 | */ |
1274 | signals[EMOJI_PICKED] = g_signal_new (signal_name: "emoji-picked" , |
1275 | G_OBJECT_CLASS_TYPE (object_class), |
1276 | signal_flags: G_SIGNAL_RUN_LAST, |
1277 | class_offset: 0, |
1278 | NULL, NULL, |
1279 | NULL, |
1280 | G_TYPE_NONE, n_params: 1, G_TYPE_STRING|G_SIGNAL_TYPE_STATIC_SCOPE); |
1281 | |
1282 | gtk_widget_class_set_template_from_resource (widget_class, resource_name: "/org/gtk/libgtk/ui/gtkemojichooser.ui" ); |
1283 | |
1284 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, search_entry); |
1285 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, stack); |
1286 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, scrolled_window); |
1287 | |
1288 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, recent.box); |
1289 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, recent.button); |
1290 | |
1291 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.box); |
1292 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.heading); |
1293 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.button); |
1294 | |
1295 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.box); |
1296 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.heading); |
1297 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.button); |
1298 | |
1299 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.box); |
1300 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.heading); |
1301 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.button); |
1302 | |
1303 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.box); |
1304 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.heading); |
1305 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.button); |
1306 | |
1307 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.box); |
1308 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.heading); |
1309 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.button); |
1310 | |
1311 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.box); |
1312 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.heading); |
1313 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.button); |
1314 | |
1315 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.box); |
1316 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.heading); |
1317 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.button); |
1318 | |
1319 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.box); |
1320 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.heading); |
1321 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.button); |
1322 | |
1323 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.box); |
1324 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.heading); |
1325 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.button); |
1326 | |
1327 | gtk_widget_class_bind_template_callback (widget_class, emoji_activated); |
1328 | gtk_widget_class_bind_template_callback (widget_class, search_changed); |
1329 | gtk_widget_class_bind_template_callback (widget_class, stop_search); |
1330 | gtk_widget_class_bind_template_callback (widget_class, pressed_cb); |
1331 | gtk_widget_class_bind_template_callback (widget_class, long_pressed_cb); |
1332 | gtk_widget_class_bind_template_callback (widget_class, keynav_failed); |
1333 | |
1334 | /** |
1335 | * GtkEmojiChooser|scroll.section: |
1336 | * @direction: 1 to scroll forward, -1 to scroll back |
1337 | * |
1338 | * Scrolls to the next or previous section. |
1339 | */ |
1340 | gtk_widget_class_install_action (widget_class, action_name: "scroll.section" , parameter_type: "i" , |
1341 | activate: gtk_emoji_chooser_scroll_section); |
1342 | |
1343 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_n, mods: GDK_CONTROL_MASK, |
1344 | action_name: "scroll.section" , format_string: "i" , 1); |
1345 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_p, mods: GDK_CONTROL_MASK, |
1346 | action_name: "scroll.section" , format_string: "i" , -1); |
1347 | } |
1348 | |
1349 | /** |
1350 | * gtk_emoji_chooser_new: |
1351 | * |
1352 | * Creates a new `GtkEmojiChooser`. |
1353 | * |
1354 | * Returns: a new `GtkEmojiChooser` |
1355 | */ |
1356 | GtkWidget * |
1357 | gtk_emoji_chooser_new (void) |
1358 | { |
1359 | return GTK_WIDGET (g_object_new (GTK_TYPE_EMOJI_CHOOSER, NULL)); |
1360 | } |
1361 | |