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