[WTF] Add makeUnique<T>, which ensures T is fast-allocated, makeUnique / makeUniqueWi...
[WebKit-https.git] / Source / WebKit / UIProcess / API / gtk / WebKitEmojiChooser.cpp
1 /*
2  * Copyright (C) 2019 Igalia S.L.
3  * Copyright (C) 2017 Red Hat, Inc.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, 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  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public License
16  * along with this library; see the file COPYING.LIB.  If not, write to
17  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  */
20
21 // GtkEmojiChooser is private in GTK 3, so this is based in the GTK code, just adapted to
22 // WebKit coding style, using some internal types from WTF to simplify the implementation
23 // and not using GtkBuilder for the UI.
24
25 #include "config.h"
26 #include "WebKitEmojiChooser.h"
27
28 #if GTK_CHECK_VERSION(3, 24, 0)
29
30 #include <glib/gi18n-lib.h>
31 #include <wtf/HashSet.h>
32 #include <wtf/RunLoop.h>
33 #include <wtf/Vector.h>
34 #include <wtf/glib/GRefPtr.h>
35 #include <wtf/glib/GUniquePtr.h>
36 #include <wtf/glib/WTFGType.h>
37 #include <wtf/text/CString.h>
38
39 enum {
40     EMOJI_PICKED,
41
42     LAST_SIGNAL
43 };
44
45 struct EmojiSection {
46     GtkWidget* heading { nullptr };
47     GtkWidget* box { nullptr };
48     GtkWidget* button { nullptr };
49     bool isEmpty { false };
50     const char* firstEmojiName { nullptr };
51 };
52
53 using SectionList = Vector<EmojiSection, 9>;
54
55 class CallbackTimer final : public RunLoop::TimerBase {
56 public:
57     CallbackTimer(Function<void()>&& callback)
58         : RunLoop::TimerBase(RunLoop::main())
59         , m_callback(WTFMove(callback))
60     {
61     }
62
63     ~CallbackTimer() = default;
64
65 private:
66     void fired() override
67     {
68         m_callback();
69     }
70
71     Function<void()> m_callback;
72 };
73
74 struct _WebKitEmojiChooserPrivate {
75     GtkWidget* stack;
76     GtkWidget* swindow;
77     GtkWidget* searchEntry;
78     SectionList sections;
79     GRefPtr<GSettings> settings;
80     HashSet<GRefPtr<GtkGesture>> gestures;
81     int emojiMaxWidth;
82     std::unique_ptr<CallbackTimer> populateSectionsTimer;
83 };
84
85 static guint signals[LAST_SIGNAL] = { 0, };
86
87 WEBKIT_DEFINE_TYPE(WebKitEmojiChooser, webkit_emoji_chooser, GTK_TYPE_POPOVER)
88
89 static void emojiPopupMenu(GtkWidget*, WebKitEmojiChooser*);
90
91 static const unsigned boxSpace = 6;
92
93 static void emojiHovered(GtkWidget* widget, GdkEvent* event)
94 {
95     if (gdk_event_get_event_type(event) == GDK_ENTER_NOTIFY)
96         gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
97     else
98         gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
99 }
100
101 static GtkWidget* webkitEmojiChooserAddEmoji(WebKitEmojiChooser* chooser, GtkFlowBox* parent, GVariant* item, bool prepend = false, gunichar modifier = 0)
102 {
103     char text[64];
104     char* textPtr = text;
105     GRefPtr<GVariant> codes = adoptGRef(g_variant_get_child_value(item, 0));
106     for (unsigned i = 0; i < g_variant_n_children(codes.get()); ++i) {
107         gunichar code;
108         g_variant_get_child(codes.get(), i, "u", &code);
109         if (!code)
110             code = modifier;
111         if (code)
112             textPtr += g_unichar_to_utf8(code, textPtr);
113     }
114     // U+FE0F is the Emoji variation selector
115     textPtr += g_unichar_to_utf8(0xFE0F, textPtr);
116     textPtr[0] = '\0';
117
118     GtkWidget* label = gtk_label_new(text);
119     PangoAttrList* attributes = pango_attr_list_new();
120     pango_attr_list_insert(attributes, pango_attr_scale_new(PANGO_SCALE_X_LARGE));
121     gtk_label_set_attributes(GTK_LABEL(label), attributes);
122     pango_attr_list_unref(attributes);
123
124     PangoLayout* layout = gtk_label_get_layout(GTK_LABEL(label));
125     PangoRectangle rect;
126     pango_layout_get_extents(layout, &rect, nullptr);
127     // Check for fallback rendering that generates too wide items.
128     if (pango_layout_get_unknown_glyphs_count(layout) || rect.width >= 1.5 * chooser->priv->emojiMaxWidth) {
129         gtk_widget_destroy(label);
130         return nullptr;
131     }
132
133     GtkWidget* child = gtk_flow_box_child_new();
134     gtk_style_context_add_class(gtk_widget_get_style_context(child), "emoji");
135     g_object_set_data_full(G_OBJECT(child), "emoji-data", g_variant_ref(item), reinterpret_cast<GDestroyNotify>(g_variant_unref));
136     if (modifier)
137         g_object_set_data(G_OBJECT(child), "modifier", GUINT_TO_POINTER(modifier));
138
139     GtkWidget* eventBox = gtk_event_box_new();
140     gtk_widget_add_events(eventBox, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
141     g_signal_connect(eventBox, "enter-notify-event", G_CALLBACK(emojiHovered), nullptr);
142     g_signal_connect(eventBox, "leave-notify-event", G_CALLBACK(emojiHovered), nullptr);
143     gtk_container_add(GTK_CONTAINER(eventBox), label);
144     gtk_widget_show(label);
145
146     gtk_container_add(GTK_CONTAINER(child), eventBox);
147     gtk_widget_show(eventBox);
148
149     gtk_flow_box_insert(parent, child, prepend ? 0 : -1);
150     gtk_widget_show(child);
151
152     return child;
153 }
154
155 static void webkitEmojiChooserAddRecentItem(WebKitEmojiChooser* chooser, GVariant* item, gunichar modifier)
156 {
157     GRefPtr<GVariant> protectItem(item);
158     GVariantBuilder builder;
159     g_variant_builder_init(&builder, G_VARIANT_TYPE("a((auss)u)"));
160     g_variant_builder_add(&builder, "(@(auss)u)", item, modifier);
161
162     auto& section = chooser->priv->sections.first();
163
164     static const unsigned maxRecentItems = 7 * 3;
165
166     GUniquePtr<GList> children(gtk_container_get_children(GTK_CONTAINER(section.box)));
167     unsigned i = 1;
168     for (auto* l = children.get(); l; l = g_list_next(l), ++i) {
169         auto* item2 = static_cast<GVariant*>(g_object_get_data(G_OBJECT(l->data), "emoji-data"));
170         auto modifier2 = static_cast<gunichar>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(l->data), "modifier")));
171         if (modifier == modifier2 && g_variant_equal(item, item2)) {
172             gtk_widget_destroy(GTK_WIDGET(l->data));
173             --i;
174             continue;
175         }
176
177         if (i >= maxRecentItems) {
178             gtk_widget_destroy(GTK_WIDGET(l->data));
179             continue;
180         }
181
182         g_variant_builder_add(&builder, "(@(auss)u)", item2, modifier2);
183     }
184
185     auto* child = webkitEmojiChooserAddEmoji(chooser, GTK_FLOW_BOX(section.box), item, true, modifier);
186     if (child)
187         g_signal_connect(child, "popup-menu", G_CALLBACK(emojiPopupMenu), chooser);
188
189     gtk_widget_show(section.box);
190     gtk_widget_set_sensitive(section.button, TRUE);
191
192     g_settings_set_value(chooser->priv->settings.get(), "recent-emoji", g_variant_builder_end(&builder));
193 }
194
195 static void emojiActivated(GtkFlowBox* box, GtkFlowBoxChild* child, WebKitEmojiChooser* chooser)
196 {
197     GtkWidget* label = gtk_bin_get_child(GTK_BIN(gtk_bin_get_child(GTK_BIN(child))));
198     GUniquePtr<char> text(g_strdup(gtk_label_get_label(GTK_LABEL(label))));
199
200     auto* item = static_cast<GVariant*>(g_object_get_data(G_OBJECT(child), "emoji-data"));
201     auto modifier = static_cast<gunichar>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(child), "modifier")));
202     webkitEmojiChooserAddRecentItem(chooser, item, modifier);
203     g_signal_emit(chooser, signals[EMOJI_PICKED], 0, text.get());
204
205     gtk_popover_popdown(GTK_POPOVER(chooser));
206 }
207
208 static bool emojiDataHasVariations(GVariant* emojiData)
209 {
210     GRefPtr<GVariant> codes = adoptGRef(g_variant_get_child_value(emojiData, 0));
211     for (size_t i = 0; i < g_variant_n_children(codes.get()); ++i) {
212         gunichar code;
213         g_variant_get_child(codes.get(), i, "u", &code);
214         if (!code)
215             return true;
216     }
217     return false;
218 }
219
220 static void webkitEmojiChooserShowVariations(WebKitEmojiChooser* chooser, GtkWidget* child)
221 {
222     if (!child)
223         return;
224
225     auto* emojiData = static_cast<GVariant*>(g_object_get_data(G_OBJECT(child), "emoji-data"));
226     if (!emojiData)
227         return;
228
229     if (!emojiDataHasVariations(emojiData))
230         return;
231
232     GtkWidget* popover = gtk_popover_new(child);
233     GtkWidget* view = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
234     gtk_style_context_add_class(gtk_widget_get_style_context(view), "view");
235     GtkWidget* box = gtk_flow_box_new();
236     g_signal_connect(box, "child-activated", G_CALLBACK(emojiActivated), chooser);
237     gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(box), TRUE);
238     gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(box), 6);
239     gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(box), 6);
240     gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(box), TRUE);
241     gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(box), GTK_SELECTION_NONE);
242     gtk_container_add(GTK_CONTAINER(view), box);
243     gtk_widget_show(box);
244     gtk_container_add(GTK_CONTAINER(popover), view);
245     gtk_widget_show(view);
246
247     webkitEmojiChooserAddEmoji(chooser, GTK_FLOW_BOX(box), emojiData);
248     for (gunichar modifier = 0x1F3FB; modifier <= 0x1F3FF; ++modifier)
249         webkitEmojiChooserAddEmoji(chooser, GTK_FLOW_BOX(box), emojiData, false, modifier);
250
251     gtk_popover_popup(GTK_POPOVER(popover));
252 }
253
254 static void emojiLongPressed(GtkGesture* gesture, double x, double y, WebKitEmojiChooser* chooser)
255 {
256     auto* box = GTK_FLOW_BOX(gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture)));
257     webkitEmojiChooserShowVariations(chooser, GTK_WIDGET(gtk_flow_box_get_child_at_pos(box, x, y)));
258 }
259
260 static void emojiPressed(GtkGesture* gesture, int, double x, double y, WebKitEmojiChooser* chooser)
261 {
262     emojiLongPressed(gesture, x, y, chooser);
263 }
264
265 static void emojiPopupMenu(GtkWidget* child, WebKitEmojiChooser* chooser)
266 {
267     webkitEmojiChooserShowVariations(chooser, child);
268 }
269
270 static void verticalAdjustmentChanged(GtkAdjustment* adjustment, WebKitEmojiChooser* chooser)
271 {
272     double value = gtk_adjustment_get_value(adjustment);
273     EmojiSection* sectionToSelect = nullptr;
274     for (auto& section : chooser->priv->sections) {
275         GtkAllocation allocation;
276         if (section.heading)
277             gtk_widget_get_allocation(section.heading, &allocation);
278         else
279             gtk_widget_get_allocation(section.box, &allocation);
280
281         if (value < allocation.y - boxSpace)
282             break;
283
284         sectionToSelect = &section;
285     }
286
287     if (!sectionToSelect)
288         sectionToSelect = &chooser->priv->sections[0];
289
290     for (auto& section : chooser->priv->sections) {
291         if (&section == sectionToSelect)
292             gtk_widget_set_state_flags(section.button, GTK_STATE_FLAG_CHECKED, FALSE);
293         else
294             gtk_widget_unset_state_flags(section.button, GTK_STATE_FLAG_CHECKED);
295     }
296 }
297
298 static GtkWidget* webkitEmojiChooserSetupSectionBox(WebKitEmojiChooser* chooser, GtkBox* parent, const char* firstEmojiName, const char* title, GtkAdjustment* adjustment, gboolean canHaveVariations = FALSE)
299 {
300     EmojiSection section;
301     section.firstEmojiName = firstEmojiName;
302     if (title) {
303         GtkWidget* label = gtk_label_new(title);
304         section.heading = label;
305         gtk_label_set_xalign(GTK_LABEL(label), 0);
306         gtk_box_pack_start(parent, label, FALSE, FALSE, 0);
307         gtk_widget_show(label);
308     }
309
310     GtkWidget* box = gtk_flow_box_new();
311     section.box = box;
312     g_signal_connect(box, "child-activated", G_CALLBACK(emojiActivated), chooser);
313     gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(box), TRUE);
314     gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(box), GTK_SELECTION_NONE);
315     gtk_container_set_focus_vadjustment(GTK_CONTAINER(box), adjustment);
316     gtk_box_pack_start(parent, box, FALSE, FALSE, 0);
317     gtk_widget_show(box);
318
319     if (canHaveVariations) {
320         GRefPtr<GtkGesture> gesture = adoptGRef(gtk_gesture_long_press_new(box));
321         g_signal_connect(gesture.get(), "pressed", G_CALLBACK(emojiLongPressed), chooser);
322         chooser->priv->gestures.add(WTFMove(gesture));
323         GRefPtr<GtkGesture> multiGesture = adoptGRef(gtk_gesture_multi_press_new(box));
324         gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(multiGesture.get()), GDK_BUTTON_SECONDARY);
325         g_signal_connect(multiGesture.get(), "pressed", G_CALLBACK(emojiPressed), chooser);
326         chooser->priv->gestures.add(WTFMove(multiGesture));
327     }
328
329     chooser->priv->sections.append(WTFMove(section));
330     return box;
331 }
332
333 static void scrollToSection(GtkButton* button, gpointer data)
334 {
335     auto* chooser = WEBKIT_EMOJI_CHOOSER(gtk_widget_get_ancestor(GTK_WIDGET(button), WEBKIT_TYPE_EMOJI_CHOOSER));
336     auto& section = chooser->priv->sections[GPOINTER_TO_UINT(data)];
337     GtkAdjustment* adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(chooser->priv->swindow));
338     if (section.heading) {
339         GtkAllocation allocation = { 0, 0, 0, 0 };
340         gtk_widget_get_allocation(section.heading, &allocation);
341         gtk_adjustment_set_value(adjustment, allocation.y - boxSpace);
342     } else
343         gtk_adjustment_set_value(adjustment, 0);
344 }
345
346 static void webkitEmojiChooserSetupSectionButton(WebKitEmojiChooser* chooser, GtkBox* parent, const char* iconName, const char* tooltip)
347 {
348     GtkWidget* button = gtk_button_new_from_icon_name(iconName, GTK_ICON_SIZE_BUTTON);
349     chooser->priv->sections.last().button = button;
350     gtk_style_context_add_class(gtk_widget_get_style_context(button), "emoji-section");
351     gtk_widget_set_tooltip_text(button, tooltip);
352     g_signal_connect(button, "clicked", G_CALLBACK(scrollToSection), GUINT_TO_POINTER(chooser->priv->sections.size() - 1));
353     gtk_box_pack_start(parent, button, FALSE, FALSE, 0);
354     gtk_widget_show(button);
355 }
356
357 static void webkitEmojiChooserSetupRecent(WebKitEmojiChooser* chooser, GtkBox* emojiBox, GtkBox* buttonBox, GtkAdjustment* adjustment)
358 {
359     GtkWidget* flowBox = webkitEmojiChooserSetupSectionBox(chooser, emojiBox, nullptr, nullptr, adjustment, true);
360     webkitEmojiChooserSetupSectionButton(chooser, buttonBox, "emoji-recent-symbolic", _("Recent"));
361
362     bool isEmpty = true;
363     GRefPtr<GVariant> variant = adoptGRef(g_settings_get_value(chooser->priv->settings.get(), "recent-emoji"));
364     GVariantIter iter;
365     g_variant_iter_init(&iter, variant.get());
366     while (GRefPtr<GVariant> item = adoptGRef(g_variant_iter_next_value(&iter))) {
367         GRefPtr<GVariant> emojiData = adoptGRef(g_variant_get_child_value(item.get(), 0));
368         gunichar modifier;
369         g_variant_get_child(item.get(), 1, "u", &modifier);
370         if (auto* child = webkitEmojiChooserAddEmoji(chooser, GTK_FLOW_BOX(flowBox), emojiData.get(), true, modifier))
371             g_signal_connect(child, "popup-menu", G_CALLBACK(emojiPopupMenu), chooser);
372         isEmpty = false;
373     }
374
375     if (isEmpty) {
376         gtk_widget_hide(flowBox);
377         gtk_widget_set_sensitive(chooser->priv->sections.first().button, FALSE);
378     }
379 }
380
381 static void webkitEmojiChooserEnsureEmptyResult(WebKitEmojiChooser* chooser)
382 {
383     if (gtk_stack_get_child_by_name(GTK_STACK(chooser->priv->stack), "empty"))
384         return;
385
386     GtkWidget* grid = gtk_grid_new();
387     gtk_grid_set_row_spacing(GTK_GRID(grid), 12);
388     gtk_widget_set_halign(grid, GTK_ALIGN_CENTER);
389     gtk_widget_set_valign(grid, GTK_ALIGN_CENTER);
390     gtk_style_context_add_class(gtk_widget_get_style_context(grid), "dim-label");
391
392     GtkWidget* image = gtk_image_new_from_icon_name("edit-find-symbolic", GTK_ICON_SIZE_DIALOG);
393     gtk_image_set_pixel_size(GTK_IMAGE(image), 72);
394     gtk_style_context_add_class(gtk_widget_get_style_context(image), "dim-label");
395     gtk_grid_attach(GTK_GRID(grid), image, 0, 0, 1, 1);
396     gtk_widget_show(image);
397
398     GtkWidget* label = gtk_label_new(_("No Results Found"));
399     PangoAttrList* attributes = pango_attr_list_new();
400     pango_attr_list_insert(attributes, pango_attr_scale_new(1.44));
401     pango_attr_list_insert(attributes, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
402     gtk_label_set_attributes(GTK_LABEL(label), attributes);
403     pango_attr_list_unref(attributes);
404     gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
405     gtk_widget_show(label);
406
407     label = gtk_label_new(_("Try a different search"));
408     gtk_style_context_add_class(gtk_widget_get_style_context(label), "dim-label");
409     gtk_grid_attach(GTK_GRID(grid), label, 0, 2, 1, 1);
410     gtk_widget_show(label);
411
412     gtk_stack_add_named(GTK_STACK(chooser->priv->stack), grid, "empty");
413     gtk_widget_show(grid);
414 }
415
416 static void webkitEmojiChooserSearchChanged(WebKitEmojiChooser* chooser)
417 {
418     for (auto& section : chooser->priv->sections) {
419         section.isEmpty = true;
420         gtk_flow_box_invalidate_filter(GTK_FLOW_BOX(section.box));
421     }
422
423     bool resultsFound = false;
424     for (auto& section : chooser->priv->sections) {
425         if (section.heading) {
426             gtk_widget_set_visible(section.heading, !section.isEmpty);
427             gtk_widget_set_visible(section.box, !section.isEmpty);
428         }
429         resultsFound = resultsFound || !section.isEmpty;
430     }
431
432     if (!resultsFound) {
433         webkitEmojiChooserEnsureEmptyResult(chooser);
434         gtk_stack_set_visible_child_name(GTK_STACK(chooser->priv->stack), "empty");
435     } else
436         gtk_stack_set_visible_child_name(GTK_STACK(chooser->priv->stack), "list");
437 }
438
439 static void webkitEmojiChooserSetupFilters(WebKitEmojiChooser* chooser)
440 {
441     for (size_t i = 0; i < chooser->priv->sections.size(); ++i) {
442         gtk_flow_box_set_filter_func(GTK_FLOW_BOX(chooser->priv->sections[i].box), [](GtkFlowBoxChild* child, gpointer userData) -> gboolean {
443             auto* chooser = WEBKIT_EMOJI_CHOOSER(gtk_widget_get_ancestor(GTK_WIDGET(child), WEBKIT_TYPE_EMOJI_CHOOSER));
444             auto& section = chooser->priv->sections[GPOINTER_TO_UINT(userData)];
445             const char* text = gtk_entry_get_text(GTK_ENTRY(chooser->priv->searchEntry));
446             if (!text || !*text) {
447                 section.isEmpty = false;
448                 return TRUE;
449             }
450
451             auto* emojiData = static_cast<GVariant*>(g_object_get_data(G_OBJECT(child), "emoji-data"));
452             if (!emojiData) {
453                 section.isEmpty = false;
454                 return TRUE;
455             }
456
457             const char* name;
458             g_variant_get_child(emojiData, 1, "&s", &name);
459             if (g_str_match_string(text, name, TRUE)) {
460                 section.isEmpty = false;
461                 return TRUE;
462             }
463
464             return FALSE;
465         }, GUINT_TO_POINTER(i), nullptr);
466     }
467 }
468
469 static void webkitEmojiChooserSetupEmojiSections(WebKitEmojiChooser* chooser, GtkBox* emojiBox, GtkBox* buttonBox)
470 {
471     static const struct {
472         const char* firstEmojiName;
473         const char* title;
474         const char* iconName;
475         bool canHaveVariations;
476     } sections[] = {
477         { "grinning face", N_("Smileys & People"), "emoji-people-symbolic", true },
478         { "selfie", N_("Body & Clothing"), "emoji-body-symbolic", true },
479         { "monkey", N_("Animals & Nature"), "emoji-nature-symbolic", false },
480         { "grapes", N_("Food & Drink"), "emoji-food-symbolic", false },
481         { "globe showing Europe-Africa", N_("Travel & Places"), "emoji-travel-symbolic", false },
482         { "jack-o-lantern", N_("Activities"), "emoji-activities-symbolic", false },
483         { "uted speaker", _("Objects"), "emoji-objects-symbolic", false },
484         { "ATM sign", N_("Symbols"), "emoji-symbols-symbolic", false },
485         { "chequered flag", _("Flags"), "emoji-flags-symbolic", false }
486     };
487
488     auto* vAdjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(chooser->priv->swindow));
489
490     GtkWidget* flowBox = nullptr;
491     for (unsigned i = 0; i < G_N_ELEMENTS(sections); ++i) {
492         auto* box = webkitEmojiChooserSetupSectionBox(chooser, emojiBox, sections[i].firstEmojiName, sections[i].title, vAdjustment, sections[i].canHaveVariations);
493         webkitEmojiChooserSetupSectionButton(chooser, buttonBox, sections[i].iconName, sections[i].title);
494         if (!i)
495             flowBox = box;
496     }
497
498     GRefPtr<GBytes> bytes = adoptGRef(g_resources_lookup_data("/org/gtk/libgtk/emoji/emoji.data", G_RESOURCE_LOOKUP_FLAGS_NONE, nullptr));
499     GRefPtr<GVariant> data = g_variant_new_from_bytes(G_VARIANT_TYPE("a(auss)"), bytes.get(), TRUE);
500     GUniquePtr<GVariantIter> iter(g_variant_iter_new(data.get()));
501
502     Function<void()> populateSections = [chooser, iter = WTFMove(iter), flowBox]() mutable {
503         auto start = MonotonicTime::now();
504         while (GRefPtr<GVariant> item = adoptGRef(g_variant_iter_next_value(iter.get()))) {
505             const char* name;
506             g_variant_get_child(item.get(), 1, "&s", &name);
507
508             auto index = chooser->priv->sections.findMatching([&name](const auto& section) {
509                 return !g_strcmp0(name, section.firstEmojiName);
510             });
511             flowBox = index == notFound ? flowBox : chooser->priv->sections[index].box;
512             auto* child = webkitEmojiChooserAddEmoji(chooser, GTK_FLOW_BOX(flowBox), item.get());
513             if (child)
514                 g_signal_connect(child, "popup-menu", G_CALLBACK(emojiPopupMenu), chooser);
515
516             if (MonotonicTime::now() - start >= 8_ms)
517                 return;
518         }
519         chooser->priv->populateSectionsTimer = nullptr;
520     };
521
522     chooser->priv->populateSectionsTimer = makeUnique<CallbackTimer>(WTFMove(populateSections));
523     chooser->priv->populateSectionsTimer->setPriority(G_PRIORITY_DEFAULT_IDLE);
524     chooser->priv->populateSectionsTimer->setName("[WebKitEmojiChooser] populate sections timer");
525     chooser->priv->populateSectionsTimer->startRepeating({ });
526 }
527
528 static void webkitEmojiChooserInitializeEmojiMaxWidth(WebKitEmojiChooser* chooser)
529 {
530     // Get a reasonable maximum width for an emoji. We do this to skip overly wide fallback
531     // rendering for certain emojis the font does not contain and therefore end up being
532     // rendered as multiple glyphs.
533     GRefPtr<PangoLayout> layout = adoptGRef(gtk_widget_create_pango_layout(GTK_WIDGET(chooser), "🙂"));
534     auto* attributes = pango_attr_list_new();
535     pango_attr_list_insert(attributes, pango_attr_scale_new(PANGO_SCALE_X_LARGE));
536     pango_layout_set_attributes(layout.get(), attributes);
537     pango_attr_list_unref(attributes);
538
539     PangoRectangle rect;
540     pango_layout_get_extents(layout.get(), &rect, nullptr);
541     chooser->priv->emojiMaxWidth = rect.width;
542 }
543
544 static void webkitEmojiChooserConstructed(GObject* object)
545 {
546     WebKitEmojiChooser* chooser = WEBKIT_EMOJI_CHOOSER(object);
547     chooser->priv->settings = adoptGRef(g_settings_new("org.gtk.Settings.EmojiChooser"));
548
549     G_OBJECT_CLASS(webkit_emoji_chooser_parent_class)->constructed(object);
550
551     webkitEmojiChooserInitializeEmojiMaxWidth(chooser);
552
553     gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(object)), "emoji-picker");
554
555     GtkWidget* mainBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
556     GtkWidget* searchEntry = gtk_search_entry_new();
557     chooser->priv->searchEntry = searchEntry;
558     g_signal_connect_swapped(searchEntry, "search-changed", G_CALLBACK(webkitEmojiChooserSearchChanged), chooser);
559     gtk_entry_set_input_hints(GTK_ENTRY(searchEntry), GTK_INPUT_HINT_NO_EMOJI);
560     gtk_box_pack_start(GTK_BOX(mainBox), searchEntry, TRUE, FALSE, 0);
561     gtk_widget_show(searchEntry);
562
563     GtkWidget* stack = gtk_stack_new();
564     chooser->priv->stack = stack;
565     GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
566     GtkWidget* swindow = gtk_scrolled_window_new(nullptr, nullptr);
567     chooser->priv->swindow = swindow;
568     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swindow), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
569     gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(swindow), 250);
570     gtk_style_context_add_class(gtk_widget_get_style_context(swindow), "view");
571     gtk_box_pack_start(GTK_BOX(box), swindow, TRUE, TRUE, 0);
572     gtk_widget_show(swindow);
573
574     GtkWidget* emojiBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
575     g_object_set(emojiBox, "margin", 6, nullptr);
576     gtk_container_add(GTK_CONTAINER(swindow), emojiBox);
577     gtk_widget_show(emojiBox);
578
579     GtkWidget* buttonBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
580     gtk_box_pack_start(GTK_BOX(box), buttonBox, TRUE, FALSE, 0);
581     gtk_widget_show(buttonBox);
582
583     GtkAdjustment* vAdjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(swindow));
584     g_signal_connect(vAdjustment, "value-changed", G_CALLBACK(verticalAdjustmentChanged), chooser);
585
586     webkitEmojiChooserSetupRecent(chooser, GTK_BOX(emojiBox), GTK_BOX(buttonBox), vAdjustment);
587
588     webkitEmojiChooserSetupEmojiSections(chooser, GTK_BOX(emojiBox), GTK_BOX(buttonBox));
589
590     gtk_widget_set_state_flags(chooser->priv->sections.first().button, GTK_STATE_FLAG_CHECKED, FALSE);
591
592     gtk_stack_add_named(GTK_STACK(stack), box, "list");
593     gtk_widget_show(box);
594
595     gtk_box_pack_start(GTK_BOX(mainBox), stack, TRUE, TRUE, 0);
596     gtk_widget_show(stack);
597
598     gtk_container_add(GTK_CONTAINER(object), mainBox);
599     gtk_widget_show(mainBox);
600
601     webkitEmojiChooserSetupFilters(chooser);
602 }
603
604 static void webkitEmojiChooserShow(GtkWidget* widget)
605 {
606     GTK_WIDGET_CLASS(webkit_emoji_chooser_parent_class)->show(widget);
607
608     WebKitEmojiChooser* chooser = WEBKIT_EMOJI_CHOOSER(widget);
609     auto* adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(chooser->priv->swindow));
610     gtk_adjustment_set_value(adjustment, 0);
611
612     gtk_entry_set_text(GTK_ENTRY(chooser->priv->searchEntry), "");
613 }
614
615 static void webkit_emoji_chooser_class_init(WebKitEmojiChooserClass* klass)
616 {
617     GObjectClass* objectClass = G_OBJECT_CLASS(klass);
618     objectClass->constructed = webkitEmojiChooserConstructed;
619
620     GtkWidgetClass* widgetClass = GTK_WIDGET_CLASS(klass);
621     widgetClass->show = webkitEmojiChooserShow;
622
623     signals[EMOJI_PICKED] = g_signal_new(
624         "emoji-picked",
625         G_OBJECT_CLASS_TYPE(objectClass),
626         G_SIGNAL_RUN_LAST,
627         0,
628         nullptr, nullptr,
629         nullptr,
630         G_TYPE_NONE, 1,
631         G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
632 }
633
634 GtkWidget* webkitEmojiChooserNew()
635 {
636     WebKitEmojiChooser* authDialog = WEBKIT_EMOJI_CHOOSER(g_object_new(WEBKIT_TYPE_EMOJI_CHOOSER, nullptr));
637     return GTK_WIDGET(authDialog);
638 }
639
640 #endif // GTK_CHECK_VERSION(3, 24, 0)