/* ZoiteChat * Copyright (C) 2026 deepend-tildeclub. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ /* ZoiteChat's own emoji picker. Replaces GTK's built-in "show-emoji-icon" * entry machinery so that every build offers the same catalog (bundled at * compile time from pinned Unicode/CLDR data, see src/common/emoji-data.h) * regardless of the GTK runtime version or distribution emoji data. * Selecting an emoji inserts the plain Unicode sequence at the cursor - * nothing about what is sent over IRC changes. */ #include #include "fe-gtk.h" #include "../common/zoitechat.h" #include "../common/zoitechatc.h" #include "../common/cfgfiles.h" #include "../common/emoji-data.h" #include "emoji-picker.h" #include "emoji-font.h" #include "preferences-persistence.h" #define EMOJI_CATEGORY_RECENT -1 #define EMOJI_RECENTS_MAX 32 #define EMOJI_SEARCH_MAX_RESULTS 256 #define EMOJI_GRID_COLUMNS 9 typedef struct { GtkWidget *button; GtkWidget *target; /* weak pointer to the input entry */ GtkWidget *popover; GtkWidget *search_entry; GtkWidget *stack; GtkWidget *grid; /* GtkFlowBox */ GtkWidget *scroller; GtkWidget *empty_label; GtkWidget *category_buttons[EMOJI_GROUP_COUNT + 1]; /* [0] = recents */ GtkWidget *tone_buttons[EMOJI_TONE_COUNT]; int category; /* EMOJI_CATEGORY_RECENT or EmojiGroup */ gboolean populated; gboolean updating_buttons; } EmojiPicker; /* ------------------------------------------------------------------------ * * Recently used emoji, shared by all windows and persisted across runs * ------------------------------------------------------------------------ */ static GPtrArray *recent_emoji = NULL; static void emoji_search_stop_cb (GtkSearchEntry *entry, gpointer userdata); static char * emoji_recents_filename (void) { return g_build_filename (get_xdir (), "emoji-recents.conf", NULL); } static void emoji_recents_load (void) { char *filename, *contents = NULL; char **lines; gsize i; if (recent_emoji) return; recent_emoji = g_ptr_array_new_with_free_func (g_free); filename = emoji_recents_filename (); if (g_file_get_contents (filename, &contents, NULL, NULL)) { lines = g_strsplit (contents, "\n", -1); for (i = 0; lines[i] && recent_emoji->len < EMOJI_RECENTS_MAX; i++) { g_strstrip (lines[i]); /* only keep sequences that exist in the bundled catalog */ if (*lines[i] && emoji_data_find_by_sequence (lines[i], NULL)) g_ptr_array_add (recent_emoji, g_strdup (lines[i])); } g_strfreev (lines); g_free (contents); } g_free (filename); } static void emoji_recents_save (void) { GString *contents = g_string_new (NULL); char *filename; gsize i; for (i = 0; i < recent_emoji->len; i++) { g_string_append (contents, g_ptr_array_index (recent_emoji, i)); g_string_append_c (contents, '\n'); } filename = emoji_recents_filename (); g_file_set_contents (filename, contents->str, contents->len, NULL); g_free (filename); g_string_free (contents, TRUE); } static void emoji_recents_add (const char *sequence) { GPtrArray *updated; gsize i; emoji_recents_load (); updated = g_ptr_array_new_with_free_func (g_free); g_ptr_array_add (updated, g_strdup (sequence)); for (i = 0; i < recent_emoji->len && updated->len < EMOJI_RECENTS_MAX; i++) { const char *old = g_ptr_array_index (recent_emoji, i); if (strcmp (old, sequence) != 0) g_ptr_array_add (updated, g_strdup (old)); } g_ptr_array_free (recent_emoji, TRUE); recent_emoji = updated; emoji_recents_save (); } /* ------------------------------------------------------------------------ * * Appearance helpers * ------------------------------------------------------------------------ */ /* One application-wide provider so every emoji cell prefers a color emoji * font without changing the fonts of surrounding widgets. The private * font map resolves ZoiteChat Emoji to the same bundled file everywhere. */ static void emoji_picker_ensure_css (void) { static gboolean done = FALSE; GtkCssProvider *provider; if (done) return; done = TRUE; provider = gtk_css_provider_new (); gtk_css_provider_load_from_data (provider, ".zc-emoji-cell {" " font-family: \"ZoiteChat Emoji\", \"Noto Color Emoji\"," " \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";" " font-size: 15pt;" " padding: 2px;" "}" ".zc-emoji-cell-small {" " font-family: \"ZoiteChat Emoji\", \"Noto Color Emoji\"," " \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";" "}", -1, NULL); gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref (provider); } static GtkWidget * emoji_label_new (const char *sequence, gboolean small_cell) { GtkWidget *label = gtk_label_new (sequence); gtk_style_context_add_class (gtk_widget_get_style_context (label), small_cell ? "zc-emoji-cell-small" : "zc-emoji-cell"); return label; } static void emoji_picker_set_a11y_name (GtkWidget *widget, const char *name) { AtkObject *accessible = gtk_widget_get_accessible (widget); if (accessible) atk_object_set_name (accessible, name); } static EmojiTone emoji_picker_current_tone (void) { int tone = prefs.hex_gui_emoji_skin_tone; if (tone < EMOJI_TONE_NONE || tone >= EMOJI_TONE_COUNT) return EMOJI_TONE_NONE; return (EmojiTone) tone; } /* "waving hand" / "waving hand (Dark skin tone)", with :shortcodes: on a * second line - used for both tooltips and accessible names. */ static char * emoji_picker_describe (const EmojiEntry *entry, EmojiTone tone) { GString *desc = g_string_new (NULL); char **aliases; gsize i; if (tone != EMOJI_TONE_NONE && entry->tone_set >= 0) g_string_append_printf (desc, "%s (%s)", entry->name, emoji_data_tone_display_name (tone)); else g_string_append (desc, entry->name); if (*entry->aliases) { g_string_append_c (desc, '\n'); aliases = g_strsplit (entry->aliases, " ", -1); for (i = 0; aliases[i]; i++) { if (i > 0) g_string_append_c (desc, ' '); g_string_append_printf (desc, ":%s:", aliases[i]); } g_strfreev (aliases); } return g_string_free (desc, FALSE); } /* ------------------------------------------------------------------------ * * Insertion * ------------------------------------------------------------------------ */ /* GtkEntry can select all text when focused; preserve its selection and * editability instead of temporarily making a read-only entry writable. */ static void emoji_picker_focus_target (EmojiPicker *picker) { GtkEditable *editable; int start, end, pos; gboolean selected; if (!picker->target) return; /* Closing a window also closes its popovers. Moving focus during that * teardown can make GtkWindow reference an already destroyed widget. */ if (gtk_widget_in_destruction (picker->target) || gtk_widget_in_destruction (gtk_widget_get_toplevel (picker->target))) return; editable = GTK_EDITABLE (picker->target); pos = gtk_editable_get_position (editable); selected = gtk_editable_get_selection_bounds (editable, &start, &end); gtk_widget_grab_focus (picker->target); if (selected) gtk_editable_select_region (editable, pos == start ? end : start, pos); else gtk_editable_set_position (editable, pos); } static void emoji_picker_insert (EmojiPicker *picker, const char *sequence) { GtkEditable *editable; int pos, start, end, max_length, length; if (!picker->target || !sequence || !*sequence) return; editable = GTK_EDITABLE (picker->target); if (!gtk_editable_get_editable (editable)) return; /* GtkEntry otherwise truncates at a code point, possibly leaving half * a flag, a dangling joiner or a skin-tone modifier. Reject as a unit. */ max_length = gtk_entry_get_max_length (GTK_ENTRY (editable)); length = gtk_entry_get_text_length (GTK_ENTRY (editable)); if (gtk_editable_get_selection_bounds (editable, &start, &end)) length -= end - start; if (length + g_utf8_strlen (sequence, -1) > (max_length ? max_length : 65535)) { gtk_widget_error_bell (picker->target); return; } gtk_editable_delete_selection (editable); pos = gtk_editable_get_position (editable); gtk_editable_insert_text (editable, sequence, strlen (sequence), &pos); gtk_editable_set_position (editable, pos); emoji_recents_add (sequence); gtk_popover_popdown (GTK_POPOVER (picker->popover)); emoji_picker_focus_target (picker); } static void emoji_picker_activate_child (EmojiPicker *picker, GtkFlowBoxChild *child) { emoji_picker_insert (picker, g_object_get_data (G_OBJECT (child), "emoji-sequence")); } static void emoji_grid_child_activated_cb (GtkFlowBox *grid, GtkFlowBoxChild *child, gpointer userdata) { emoji_picker_activate_child (userdata, child); } /* ------------------------------------------------------------------------ * * Grid contents * ------------------------------------------------------------------------ */ static void emoji_grid_add (EmojiPicker *picker, const EmojiEntry *entry, const char *sequence, EmojiTone tone) { GtkWidget *child, *label; char *desc; label = emoji_label_new (sequence, FALSE); child = gtk_flow_box_child_new (); gtk_container_add (GTK_CONTAINER (child), label); g_object_set_data_full (G_OBJECT (child), "emoji-sequence", g_strdup (sequence), g_free); desc = emoji_picker_describe (entry, tone); gtk_widget_set_tooltip_text (child, desc); emoji_picker_set_a11y_name (child, desc); g_free (desc); gtk_container_add (GTK_CONTAINER (picker->grid), child); } static void emoji_picker_refresh (EmojiPicker *picker) { const char *text = gtk_entry_get_text (GTK_ENTRY (picker->search_entry)); EmojiTone tone = emoji_picker_current_tone (); GtkAdjustment *vadj; gboolean any = FALSE; gsize i, results = 0; gtk_container_foreach (GTK_CONTAINER (picker->grid), (GtkCallback) gtk_widget_destroy, NULL); if (text && *text) { char **tokens = emoji_search_tokenize (text); if (tokens) { for (i = 0; i < emoji_data_count () && results < EMOJI_SEARCH_MAX_RESULTS; i++) { const EmojiEntry *entry = emoji_data_entry (i); if (!emoji_entry_matches (entry, tokens)) continue; emoji_grid_add (picker, entry, emoji_entry_sequence_for_tone (entry, tone), tone); results++; } g_strfreev (tokens); any = results > 0; } gtk_label_set_text (GTK_LABEL (picker->empty_label), _("No emoji found")); } else if (picker->category == EMOJI_CATEGORY_RECENT) { emoji_recents_load (); for (i = 0; i < recent_emoji->len; i++) { const char *sequence = g_ptr_array_index (recent_emoji, i); EmojiTone seq_tone; const EmojiEntry *entry = emoji_data_find_by_sequence (sequence, &seq_tone); if (!entry) continue; emoji_grid_add (picker, entry, sequence, seq_tone); any = TRUE; } gtk_label_set_text (GTK_LABEL (picker->empty_label), _("No recently used emoji")); } else { for (i = 0; i < emoji_data_count (); i++) { const EmojiEntry *entry = emoji_data_entry (i); if (entry->group != picker->category) continue; emoji_grid_add (picker, entry, emoji_entry_sequence_for_tone (entry, tone), tone); any = TRUE; } } gtk_widget_show_all (picker->grid); gtk_stack_set_visible_child_name (GTK_STACK (picker->stack), any ? "grid" : "empty"); vadj = gtk_scrolled_window_get_vadjustment ( GTK_SCROLLED_WINDOW (picker->scroller)); gtk_adjustment_set_value (vadj, gtk_adjustment_get_lower (vadj)); } /* ------------------------------------------------------------------------ * * Categories * ------------------------------------------------------------------------ */ static void emoji_picker_sync_category_buttons (EmojiPicker *picker) { gsize i; picker->updating_buttons = TRUE; for (i = 0; i < G_N_ELEMENTS (picker->category_buttons); i++) { int category = (int) i - 1; gtk_toggle_button_set_active ( GTK_TOGGLE_BUTTON (picker->category_buttons[i]), category == picker->category); } picker->updating_buttons = FALSE; } static void emoji_category_toggled_cb (GtkToggleButton *button, gpointer userdata) { EmojiPicker *picker = userdata; int category = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "emoji-category")); if (picker->updating_buttons) return; if (!gtk_toggle_button_get_active (button)) { /* the active category cannot be untoggled */ if (picker->category == category) emoji_picker_sync_category_buttons (picker); return; } picker->category = category; emoji_picker_sync_category_buttons (picker); /* leave search mode; clearing the entry triggers the refresh */ if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry))) gtk_entry_set_text (GTK_ENTRY (picker->search_entry), ""); else emoji_picker_refresh (picker); } static GtkWidget * emoji_category_button_new (EmojiPicker *picker, int category, const char *icon_sequence, const char *name) { GtkWidget *button = gtk_toggle_button_new (); gtk_container_add (GTK_CONTAINER (button), emoji_label_new (icon_sequence, TRUE)); gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); gtk_widget_set_tooltip_text (button, name); emoji_picker_set_a11y_name (button, name); g_object_set_data (G_OBJECT (button), "emoji-category", GINT_TO_POINTER (category)); g_signal_connect (G_OBJECT (button), "toggled", G_CALLBACK (emoji_category_toggled_cb), picker); return button; } /* ------------------------------------------------------------------------ * * Skin tones * ------------------------------------------------------------------------ */ #define EMOJI_TONE_SAMPLE "\342\234\213" /* ✋ raised hand */ static void emoji_tone_toggled_cb (GtkToggleButton *button, gpointer userdata) { EmojiPicker *picker = userdata; int tone = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "emoji-tone")); if (!gtk_toggle_button_get_active (button)) return; if (prefs.hex_gui_emoji_skin_tone == tone) return; prefs.hex_gui_emoji_skin_tone = tone; preferences_persistence_save_all (); emoji_picker_refresh (picker); } static GtkWidget * emoji_tone_row_new (EmojiPicker *picker) { GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); GtkWidget *button; GSList *group = NULL; const EmojiEntry *sample = emoji_data_find_by_sequence (EMOJI_TONE_SAMPLE, NULL); int tone; gtk_widget_set_halign (box, GTK_ALIGN_CENTER); for (tone = EMOJI_TONE_NONE; tone < EMOJI_TONE_COUNT; tone++) { const char *sequence = EMOJI_TONE_SAMPLE; if (sample) sequence = emoji_entry_sequence_for_tone (sample, tone); button = gtk_radio_button_new (group); group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button)); gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (button), FALSE); gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); gtk_container_add (GTK_CONTAINER (button), emoji_label_new (sequence, TRUE)); gtk_widget_set_tooltip_text (button, emoji_data_tone_display_name (tone)); emoji_picker_set_a11y_name (button, emoji_data_tone_display_name (tone)); g_object_set_data (G_OBJECT (button), "emoji-tone", GINT_TO_POINTER (tone)); picker->tone_buttons[tone] = button; if (emoji_picker_current_tone () == tone) gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE); g_signal_connect (G_OBJECT (button), "toggled", G_CALLBACK (emoji_tone_toggled_cb), picker); gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0); } return box; } /* the pref can also change externally (/set gui_emoji_skin_tone) */ static void emoji_picker_sync_tone_buttons (EmojiPicker *picker) { EmojiTone tone = emoji_picker_current_tone (); gtk_toggle_button_set_active ( GTK_TOGGLE_BUTTON (picker->tone_buttons[tone]), TRUE); } /* ------------------------------------------------------------------------ * * Search * ------------------------------------------------------------------------ */ static void emoji_search_changed_cb (GtkSearchEntry *entry, gpointer userdata) { emoji_picker_refresh (userdata); } static void emoji_search_activate_cb (GtkEntry *entry, gpointer userdata) { EmojiPicker *picker = userdata; GtkFlowBoxChild *child; /* search-changed is delayed by GTK; Enter must use the current query. */ emoji_picker_refresh (picker); child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0); /* Enter inserts the first hit */ if (child) emoji_picker_activate_child (picker, child); } static gboolean emoji_search_key_press_cb (GtkWidget *entry, GdkEventKey *event, gpointer userdata) { EmojiPicker *picker = userdata; GtkFlowBoxChild *child; if (event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down) { emoji_picker_refresh (picker); child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0); if (child) { gtk_widget_grab_focus (GTK_WIDGET (child)); return TRUE; } } return FALSE; } /* ------------------------------------------------------------------------ * * Popover assembly * ------------------------------------------------------------------------ */ static void emoji_picker_build_contents (EmojiPicker *picker) { GtkWidget *vbox, *categories, *button; int group; emoji_picker_ensure_css (); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); g_object_set (vbox, "margin", 6, NULL); picker->search_entry = gtk_search_entry_new (); gtk_entry_set_placeholder_text (GTK_ENTRY (picker->search_entry), _("Search emoji")); emoji_picker_set_a11y_name (picker->search_entry, _("Search emoji")); g_signal_connect (G_OBJECT (picker->search_entry), "search-changed", G_CALLBACK (emoji_search_changed_cb), picker); g_signal_connect (G_OBJECT (picker->search_entry), "stop-search", G_CALLBACK (emoji_search_stop_cb), picker); g_signal_connect (G_OBJECT (picker->search_entry), "activate", G_CALLBACK (emoji_search_activate_cb), picker); g_signal_connect (G_OBJECT (picker->search_entry), "key-press-event", G_CALLBACK (emoji_search_key_press_cb), picker); gtk_box_pack_start (GTK_BOX (vbox), picker->search_entry, FALSE, FALSE, 0); categories = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_set_homogeneous (GTK_BOX (categories), TRUE); picker->category_buttons[0] = emoji_category_button_new (picker, EMOJI_CATEGORY_RECENT, "\360\237\225\230" /* 🕘 */, _("Recently Used")); gtk_box_pack_start (GTK_BOX (categories), picker->category_buttons[0], TRUE, TRUE, 0); for (group = 0; group < EMOJI_GROUP_COUNT; group++) { button = emoji_category_button_new (picker, group, emoji_data_group_icon_sequence (group), emoji_data_group_display_name (group)); picker->category_buttons[group + 1] = button; gtk_box_pack_start (GTK_BOX (categories), button, TRUE, TRUE, 0); } gtk_box_pack_start (GTK_BOX (vbox), categories, FALSE, FALSE, 0); picker->grid = gtk_flow_box_new (); gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (picker->grid), GTK_SELECTION_NONE); gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (picker->grid), TRUE); gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (picker->grid), EMOJI_GRID_COLUMNS); gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (picker->grid), EMOJI_GRID_COLUMNS); gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (picker->grid), TRUE); gtk_widget_set_valign (picker->grid, GTK_ALIGN_START); g_signal_connect (G_OBJECT (picker->grid), "child-activated", G_CALLBACK (emoji_grid_child_activated_cb), picker); picker->scroller = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (picker->scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_min_content_height ( GTK_SCROLLED_WINDOW (picker->scroller), 300); gtk_container_add (GTK_CONTAINER (picker->scroller), picker->grid); picker->empty_label = gtk_label_new (_("No emoji found")); gtk_style_context_add_class ( gtk_widget_get_style_context (picker->empty_label), "dim-label"); picker->stack = gtk_stack_new (); gtk_widget_set_size_request (picker->stack, 380, -1); gtk_stack_add_named (GTK_STACK (picker->stack), picker->scroller, "grid"); gtk_stack_add_named (GTK_STACK (picker->stack), picker->empty_label, "empty"); gtk_box_pack_start (GTK_BOX (vbox), picker->stack, TRUE, TRUE, 0); gtk_box_pack_start (GTK_BOX (vbox), emoji_tone_row_new (picker), FALSE, FALSE, 0); gtk_container_add (GTK_CONTAINER (picker->popover), vbox); gtk_widget_show_all (vbox); emoji_picker_sync_category_buttons (picker); } static void emoji_popover_show_cb (GtkWidget *popover, gpointer userdata) { EmojiPicker *picker = userdata; if (!picker->populated) { picker->populated = TRUE; emoji_picker_build_contents (picker); } else if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry))) { gtk_entry_set_text (GTK_ENTRY (picker->search_entry), ""); } gtk_widget_grab_focus (picker->search_entry); emoji_picker_sync_tone_buttons (picker); emoji_picker_refresh (picker); } static void emoji_search_stop_cb (GtkSearchEntry *entry, gpointer userdata) { EmojiPicker *picker = userdata; gtk_popover_popdown (GTK_POPOVER (picker->popover)); } static void emoji_entry_insert_cb (GtkEntry *entry, GtkWidget *button) { /* Covers GtkEntry's context menu and Ctrl+period/Ctrl+semicolon. */ g_signal_stop_emission_by_name (entry, "insert-emoji"); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE); } static const char * emoji_picker_button_icon_name (void) { static const char *icon_names[] = { "face-smile-symbolic", "face-smile", "insert-emoticon-symbolic", "insert-emoticon", "zc-menu-emoji", }; GtkIconTheme *theme = gtk_icon_theme_get_default (); gsize i; if (!theme) return NULL; for (i = 0; i < G_N_ELEMENTS (icon_names); i++) { if (gtk_icon_theme_has_icon (theme, icon_names[i])) return icon_names[i]; } return NULL; } static void emoji_picker_free (gpointer data) { EmojiPicker *picker = data; if (picker->target) g_object_remove_weak_pointer (G_OBJECT (picker->target), (gpointer *) &picker->target); g_free (picker); } GtkWidget * emoji_picker_button_new (GtkWidget *target_entry) { EmojiPicker *picker; const char *icon_name; g_return_val_if_fail (GTK_IS_ENTRY (target_entry), NULL); picker = g_new0 (EmojiPicker, 1); picker->target = target_entry; picker->category = EMOJI_GROUP_SMILEYS_EMOTION; g_object_add_weak_pointer (G_OBJECT (target_entry), (gpointer *) &picker->target); picker->button = gtk_menu_button_new (); emoji_font_apply (picker->button); gtk_button_set_relief (GTK_BUTTON (picker->button), GTK_RELIEF_NONE); gtk_widget_set_can_focus (picker->button, TRUE); gtk_widget_set_focus_on_click (picker->button, FALSE); gtk_widget_set_tooltip_text (picker->button, _("Insert Emoji")); emoji_picker_set_a11y_name (picker->button, _("Insert Emoji")); icon_name = emoji_picker_button_icon_name (); if (icon_name) { gtk_button_set_image (GTK_BUTTON (picker->button), gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU)); } else { /* no themed icon anywhere: fall back to an emoji label so the * button never turns into an unusable placeholder */ emoji_picker_ensure_css (); gtk_container_add (GTK_CONTAINER (picker->button), emoji_label_new ("\360\237\231\202" /* 🙂 */, TRUE)); } /* the popover shell is cheap; its contents are built on first open so * startup cost stays unchanged */ picker->popover = gtk_popover_new (picker->button); emoji_font_apply (picker->popover); g_object_set_data_full (G_OBJECT (picker->popover), "emoji-picker", picker, emoji_picker_free); /* GtkEntry added this action in 3.22.27; earlier supported GTK 3.22 * runtimes still use the button without emitting an unknown signal. */ if (g_signal_lookup ("insert-emoji", GTK_TYPE_ENTRY)) g_signal_connect_object (target_entry, "insert-emoji", G_CALLBACK (emoji_entry_insert_cb), picker->button, 0); gtk_popover_set_position (GTK_POPOVER (picker->popover), GTK_POS_TOP); g_signal_connect (G_OBJECT (picker->popover), "show", G_CALLBACK (emoji_popover_show_cb), picker); gtk_menu_button_set_popover (GTK_MENU_BUTTON (picker->button), picker->popover); return picker->button; }