From 5a2f74f9e3aed8b6d98792fa4dff37367bcf45c1 Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Fri, 11 Oct 2019 13:43:10 +0200 Subject: [PATCH] Bug 789267 - Add Insert->Emoji into message composer Closes https://bugzilla.gnome.org/show_bug.cgi?id=789267 --- po/POTFILES.in | 1 + src/e-util/CMakeLists.txt | 2 + src/e-util/e-gtkemojichooser.c | 908 ++++++++++++++++++++++++++++ src/e-util/e-gtkemojichooser.h | 45 ++ src/e-util/e-html-editor-actions.c | 48 ++ src/e-util/e-html-editor-actions.h | 2 + src/e-util/e-html-editor-manager.ui | 1 + src/e-util/e-html-editor-private.h | 2 + 8 files changed, 1009 insertions(+) create mode 100644 src/e-util/e-gtkemojichooser.c create mode 100644 src/e-util/e-gtkemojichooser.h diff --git a/po/POTFILES.in b/po/POTFILES.in index 6443ac9378..a9aed0ef59 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -209,6 +209,7 @@ src/e-util/e-filter-option.c src/e-util/e-filter-part.c src/e-util/e-filter-rule.c src/e-util/e-focus-tracker.c +src/e-util/e-gtkemojichooser.c src/e-util/e-html-editor-actions.c src/e-util/e-html-editor.c src/e-util/e-html-editor-cell-dialog.c diff --git a/src/e-util/CMakeLists.txt b/src/e-util/CMakeLists.txt index eb9523d37a..b8de2be054 100644 --- a/src/e-util/CMakeLists.txt +++ b/src/e-util/CMakeLists.txt @@ -133,6 +133,8 @@ set(SOURCES e-filter-part.c e-filter-rule.c e-focus-tracker.c + e-gtkemojichooser.h + e-gtkemojichooser.c e-html-editor-actions.c e-html-editor-cell-dialog.c e-html-editor-dialog.c diff --git a/src/e-util/e-gtkemojichooser.c b/src/e-util/e-gtkemojichooser.c new file mode 100644 index 0000000000..7839e4db29 --- /dev/null +++ b/src/e-util/e-gtkemojichooser.c @@ -0,0 +1,908 @@ +/* + * Copyright 2017, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "evolution-config.h" + +/* Copied and adapted a bit from gtk+'s gtkemojichooser.h, + waiting for it to be made public: + https://gitlab.gnome.org/GNOME/gtk/issues/86 +*/ + +#include +#include + +#include "e-gtkemojichooser.h" + +#define BOX_SPACE 6 + +typedef struct { + GtkWidget *box; + GtkWidget *heading; + GtkWidget *button; + const char *first; + gunichar label; + gboolean empty; +} EmojiSection; + +struct _EGtkEmojiChooser +{ + GtkPopover parent_instance; + + GtkWidget *search_entry; + GtkWidget *stack; + GtkWidget *scrolled_window; + + int emoji_max_width; + + EmojiSection recent; + EmojiSection people; + EmojiSection body; + EmojiSection nature; + EmojiSection food; + EmojiSection travel; + EmojiSection activities; + EmojiSection objects; + EmojiSection symbols; + EmojiSection flags; + + GtkGesture *recent_long_press; + GtkGesture *recent_multi_press; + GtkGesture *people_long_press; + GtkGesture *people_multi_press; + GtkGesture *body_long_press; + GtkGesture *body_multi_press; + + GVariant *data; + GtkWidget *box; + GVariantIter *iter; + guint populate_idle; + + GSettings *settings; +}; + +struct _EGtkEmojiChooserClass { + GtkPopoverClass parent_class; +}; + +enum { + EMOJI_PICKED, + LAST_SIGNAL +}; + +static int signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (EGtkEmojiChooser, e_gtk_emoji_chooser, GTK_TYPE_POPOVER) + +static void +e_gtk_emoji_chooser_finalize (GObject *object) +{ + EGtkEmojiChooser *chooser = E_GTK_EMOJI_CHOOSER (object); + + if (chooser->populate_idle) + g_source_remove (chooser->populate_idle); + + g_variant_unref (chooser->data); + g_object_unref (chooser->settings); + + g_clear_object (&chooser->recent_long_press); + g_clear_object (&chooser->recent_multi_press); + g_clear_object (&chooser->people_long_press); + g_clear_object (&chooser->people_multi_press); + g_clear_object (&chooser->body_long_press); + g_clear_object (&chooser->body_multi_press); + + G_OBJECT_CLASS (e_gtk_emoji_chooser_parent_class)->finalize (object); +} + +static void +scroll_to_section (GtkButton *button, + gpointer data) +{ + EmojiSection *section = data; + EGtkEmojiChooser *chooser; + GtkAdjustment *adj; + GtkAllocation alloc = { 0, 0, 0, 0 }; + + chooser = E_GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (button), E_GTK_TYPE_EMOJI_CHOOSER)); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + + if (section->heading) + gtk_widget_get_allocation (section->heading, &alloc); + + gtk_adjustment_set_value (adj, alloc.y - BOX_SPACE); +} + +static void +add_emoji (GtkWidget *box, + gboolean prepend, + GVariant *item, + gunichar modifier, + EGtkEmojiChooser *chooser); + +#define MAX_RECENT (7*3) + +static void +populate_recent_section (EGtkEmojiChooser *chooser) +{ + GVariant *variant; + GVariant *item; + GVariantIter iter; + gboolean empty = FALSE; + + variant = g_settings_get_value (chooser->settings, "recent-emoji"); + g_variant_iter_init (&iter, variant); + while ((item = g_variant_iter_next_value (&iter))) + { + GVariant *emoji_data; + gunichar modifier; + + emoji_data = g_variant_get_child_value (item, 0); + g_variant_get_child (item, 1, "u", &modifier); + add_emoji (chooser->recent.box, FALSE, emoji_data, modifier, chooser); + g_variant_unref (emoji_data); + g_variant_unref (item); + empty = FALSE; + } + + if (!empty) + { + gtk_widget_show (chooser->recent.box); + gtk_widget_set_sensitive (chooser->recent.button, TRUE); + } + g_variant_unref (variant); +} + +static void +add_recent_item (EGtkEmojiChooser *chooser, + GVariant *item, + gunichar modifier) +{ + GList *children, *l; + int i; + GVariantBuilder builder; + + g_variant_ref (item); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a((auss)u)")); + g_variant_builder_add (&builder, "(@(auss)u)", item, modifier); + + children = gtk_container_get_children (GTK_CONTAINER (chooser->recent.box)); + for (l = children, i = 1; l; l = l->next, i++) + { + GVariant *item2 = g_object_get_data (G_OBJECT (l->data), "emoji-data"); + gunichar modifier2 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (l->data), "modifier")); + + if (modifier == modifier2 && g_variant_equal (item, item2)) + { + gtk_widget_destroy (GTK_WIDGET (l->data)); + i--; + continue; + } + if (i >= MAX_RECENT) + { + gtk_widget_destroy (GTK_WIDGET (l->data)); + continue; + } + + g_variant_builder_add (&builder, "(@(auss)u)", item2, modifier2); + } + g_list_free (children); + + add_emoji (chooser->recent.box, TRUE, item, modifier, chooser); + + /* Enable recent */ + gtk_widget_show (chooser->recent.box); + gtk_widget_set_sensitive (chooser->recent.button, TRUE); + + g_settings_set_value (chooser->settings, "recent-emoji", g_variant_builder_end (&builder)); + + g_variant_unref (item); +} + +static void +emoji_activated (GtkFlowBox *box, + GtkFlowBoxChild *child, + gpointer data) +{ + EGtkEmojiChooser *chooser = data; + char *text; + GtkWidget *ebox; + GtkWidget *label; + GVariant *item; + gunichar modifier; + + gtk_popover_popdown (GTK_POPOVER (chooser)); + + ebox = gtk_bin_get_child (GTK_BIN (child)); + label = gtk_bin_get_child (GTK_BIN (ebox)); + text = g_strdup (gtk_label_get_label (GTK_LABEL (label))); + + item = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data"); + modifier = (gunichar) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (child), "modifier")); + add_recent_item (chooser, item, modifier); + + g_signal_emit (data, signals[EMOJI_PICKED], 0, text); + g_free (text); +} + +static gboolean +has_variations (GVariant *emoji_data) +{ + GVariant *codes; + int i; + gboolean has_variations; + + has_variations = FALSE; + codes = g_variant_get_child_value (emoji_data, 0); + for (i = 0; i < g_variant_n_children (codes); i++) + { + gunichar code; + g_variant_get_child (codes, i, "u", &code); + if (code == 0) + { + has_variations = TRUE; + break; + } + } + g_variant_unref (codes); + + return has_variations; +} + +static void +show_variations (EGtkEmojiChooser *chooser, + GtkWidget *child) +{ + GtkWidget *popover; + GtkWidget *view; + GtkWidget *box; + GVariant *emoji_data; + GtkWidget *parent_popover; + gunichar modifier; + + if (!child) + return; + + emoji_data = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data"); + if (!emoji_data) + return; + + if (!has_variations (emoji_data)) + return; + + parent_popover = gtk_widget_get_ancestor (child, GTK_TYPE_POPOVER); + popover = gtk_popover_new (child); + view = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_style_context_add_class (gtk_widget_get_style_context (view), "view"); + box = gtk_flow_box_new (); + gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (box), TRUE); + gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (box), 6); + gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (box), 6); + gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (box), TRUE); + gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (box), GTK_SELECTION_NONE); + gtk_container_add (GTK_CONTAINER (popover), view); + gtk_container_add (GTK_CONTAINER (view), box); + + g_signal_connect (box, "child-activated", G_CALLBACK (emoji_activated), parent_popover); + + add_emoji (box, FALSE, emoji_data, 0, chooser); + for (modifier = 0x1f3fb; modifier <= 0x1f3ff; modifier++) + add_emoji (box, FALSE, emoji_data, modifier, chooser); + + gtk_widget_show_all (view); + gtk_popover_popup (GTK_POPOVER (popover)); +} + +static void +update_hover (GtkWidget *widget, + GdkEvent *event, + gpointer data) +{ + if (event->type == GDK_ENTER_NOTIFY) + gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_PRELIGHT, FALSE); + else + gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_PRELIGHT); +} + +static void +long_pressed_cb (GtkGesture *gesture, + double x, + double y, + gpointer data) +{ + EGtkEmojiChooser *chooser = data; + GtkWidget *box; + GtkWidget *child; + + box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); + child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y)); + show_variations (chooser, child); +} + +static void +pressed_cb (GtkGesture *gesture, + int n_press, + double x, + double y, + gpointer data) +{ + EGtkEmojiChooser *chooser = data; + GtkWidget *box; + GtkWidget *child; + + box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); + child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y)); + show_variations (chooser, child); +} + +static gboolean +popup_menu (GtkWidget *widget, + gpointer data) +{ + EGtkEmojiChooser *chooser = data; + + show_variations (chooser, widget); + return TRUE; +} + +static void +add_emoji (GtkWidget *box, + gboolean prepend, + GVariant *item, + gunichar modifier, + EGtkEmojiChooser *chooser) +{ + GtkWidget *child; + GtkWidget *ebox; + GtkWidget *label; + PangoAttrList *attrs; + GVariant *codes; + char text[64]; + char *p = text; + int i; + PangoLayout *layout; + PangoRectangle rect; + + codes = g_variant_get_child_value (item, 0); + for (i = 0; i < g_variant_n_children (codes); i++) + { + gunichar code; + + g_variant_get_child (codes, i, "u", &code); + if (code == 0) + code = modifier; + if (code != 0) + p += g_unichar_to_utf8 (code, p); + } + g_variant_unref (codes); + p += g_unichar_to_utf8 (0xFE0F, p); /* U+FE0F is the Emoji variation selector */ + p[0] = 0; + + label = gtk_label_new (text); + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE)); + gtk_label_set_attributes (GTK_LABEL (label), attrs); + pango_attr_list_unref (attrs); + + layout = gtk_label_get_layout (GTK_LABEL (label)); + pango_layout_get_extents (layout, &rect, NULL); + + /* Check for fallback rendering that generates too wide items */ + if (pango_layout_get_unknown_glyphs_count (layout) > 0 || + rect.width >= 1.5 * chooser->emoji_max_width) + { + gtk_widget_destroy (label); + return; + } + + child = gtk_flow_box_child_new (); + gtk_style_context_add_class (gtk_widget_get_style_context (child), "emoji"); + g_object_set_data_full (G_OBJECT (child), "emoji-data", + g_variant_ref (item), + (GDestroyNotify)g_variant_unref); + if (modifier != 0) + g_object_set_data (G_OBJECT (child), "modifier", GUINT_TO_POINTER (modifier)); + + ebox = gtk_event_box_new (); + gtk_widget_add_events (ebox, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); + g_signal_connect (ebox, "enter-notify-event", G_CALLBACK (update_hover), FALSE); + g_signal_connect (ebox, "leave-notify-event", G_CALLBACK (update_hover), FALSE); + gtk_container_add (GTK_CONTAINER (child), ebox); + gtk_container_add (GTK_CONTAINER (ebox), label); + gtk_widget_show_all (child); + + if (chooser) + g_signal_connect (child, "popup-menu", G_CALLBACK (popup_menu), chooser); + + gtk_flow_box_insert (GTK_FLOW_BOX (box), child, prepend ? 0 : -1); +} + +static gboolean +populate_emoji_chooser (gpointer data) +{ + EGtkEmojiChooser *chooser = data; + GBytes *bytes = NULL; + GVariant *item; + guint64 start, now; + + start = g_get_monotonic_time (); + + if (!chooser->data) + { + bytes = g_resources_lookup_data ("/org/gtk/libgtk/emoji/emoji.data", 0, NULL); + chooser->data = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE ("a(auss)"), bytes, TRUE)); + } + + if (!chooser->iter) + { + chooser->iter = g_variant_iter_new (chooser->data); + chooser->box = chooser->people.box; + } + while ((item = g_variant_iter_next_value (chooser->iter))) + { + const char *name; + + g_variant_get_child (item, 1, "&s", &name); + + if (strcmp (name, chooser->body.first) == 0) + chooser->box = chooser->body.box; + else if (strcmp (name, chooser->nature.first) == 0) + chooser->box = chooser->nature.box; + else if (strcmp (name, chooser->food.first) == 0) + chooser->box = chooser->food.box; + else if (strcmp (name, chooser->travel.first) == 0) + chooser->box = chooser->travel.box; + else if (strcmp (name, chooser->activities.first) == 0) + chooser->box = chooser->activities.box; + else if (strcmp (name, chooser->objects.first) == 0) + chooser->box = chooser->objects.box; + else if (strcmp (name, chooser->symbols.first) == 0) + chooser->box = chooser->symbols.box; + else if (strcmp (name, chooser->flags.first) == 0) + chooser->box = chooser->flags.box; + + add_emoji (chooser->box, FALSE, item, 0, chooser); + g_variant_unref (item); + + now = g_get_monotonic_time (); + if (now > start + 8000) + return G_SOURCE_CONTINUE; + } + + /* We scroll to the top on show, so check the right button for the 1st time */ + gtk_widget_set_state_flags (chooser->recent.button, GTK_STATE_FLAG_CHECKED, FALSE); + + g_variant_iter_free (chooser->iter); + chooser->iter = NULL; + chooser->box = NULL; + chooser->populate_idle = 0; + + return G_SOURCE_REMOVE; +} + +static void +adj_value_changed (GtkAdjustment *adj, + gpointer data) +{ + EGtkEmojiChooser *chooser = data; + double value = gtk_adjustment_get_value (adj); + EmojiSection const *sections[] = { + &chooser->recent, + &chooser->people, + &chooser->body, + &chooser->nature, + &chooser->food, + &chooser->travel, + &chooser->activities, + &chooser->objects, + &chooser->symbols, + &chooser->flags, + }; + EmojiSection const *select_section = sections[0]; + gsize i; + + /* Figure out which section the current scroll position is within */ + for (i = 0; i < G_N_ELEMENTS (sections); ++i) + { + EmojiSection const *section = sections[i]; + GtkAllocation alloc; + + if (section->heading) + gtk_widget_get_allocation (section->heading, &alloc); + else + gtk_widget_get_allocation (section->box, &alloc); + + if (value < alloc.y - BOX_SPACE) + break; + + select_section = section; + } + + /* Un/Check the section buttons accordingly */ + for (i = 0; i < G_N_ELEMENTS (sections); ++i) + { + EmojiSection const *section = sections[i]; + + if (section == select_section) + gtk_widget_set_state_flags (section->button, GTK_STATE_FLAG_CHECKED, FALSE); + else + gtk_widget_unset_state_flags (section->button, GTK_STATE_FLAG_CHECKED); + } +} + +static gboolean +filter_func (GtkFlowBoxChild *child, + gpointer data) +{ + EmojiSection *section = data; + EGtkEmojiChooser *chooser; + GVariant *emoji_data; + const char *text; + const char *name; + gboolean res; + + res = TRUE; + + chooser = E_GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), E_GTK_TYPE_EMOJI_CHOOSER)); + text = gtk_entry_get_text (GTK_ENTRY (chooser->search_entry)); + emoji_data = (GVariant *) g_object_get_data (G_OBJECT (child), "emoji-data"); + + if (text[0] == 0) + goto out; + + if (!emoji_data) + goto out; + + g_variant_get_child (emoji_data, 1, "&s", &name); + res = g_str_match_string (text, name, TRUE); + +out: + if (res) + section->empty = FALSE; + + return res; +} + +static void +invalidate_section (EmojiSection *section) +{ + section->empty = TRUE; + gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (section->box)); +} + +static void +update_headings (EGtkEmojiChooser *chooser) +{ + gtk_widget_set_visible (chooser->people.heading, !chooser->people.empty); + gtk_widget_set_visible (chooser->people.box, !chooser->people.empty); + gtk_widget_set_visible (chooser->body.heading, !chooser->body.empty); + gtk_widget_set_visible (chooser->body.box, !chooser->body.empty); + gtk_widget_set_visible (chooser->nature.heading, !chooser->nature.empty); + gtk_widget_set_visible (chooser->nature.box, !chooser->nature.empty); + gtk_widget_set_visible (chooser->food.heading, !chooser->food.empty); + gtk_widget_set_visible (chooser->food.box, !chooser->food.empty); + gtk_widget_set_visible (chooser->travel.heading, !chooser->travel.empty); + gtk_widget_set_visible (chooser->travel.box, !chooser->travel.empty); + gtk_widget_set_visible (chooser->activities.heading, !chooser->activities.empty); + gtk_widget_set_visible (chooser->activities.box, !chooser->activities.empty); + gtk_widget_set_visible (chooser->objects.heading, !chooser->objects.empty); + gtk_widget_set_visible (chooser->objects.box, !chooser->objects.empty); + gtk_widget_set_visible (chooser->symbols.heading, !chooser->symbols.empty); + gtk_widget_set_visible (chooser->symbols.box, !chooser->symbols.empty); + gtk_widget_set_visible (chooser->flags.heading, !chooser->flags.empty); + gtk_widget_set_visible (chooser->flags.box, !chooser->flags.empty); + + if (chooser->recent.empty && chooser->people.empty && + chooser->body.empty && chooser->nature.empty && + chooser->food.empty && chooser->travel.empty && + chooser->activities.empty && chooser->objects.empty && + chooser->symbols.empty && chooser->flags.empty) + gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "empty"); + else + gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "list"); +} + +static void +search_changed (GtkEntry *entry, + gpointer data) +{ + EGtkEmojiChooser *chooser = data; + + invalidate_section (&chooser->recent); + invalidate_section (&chooser->people); + invalidate_section (&chooser->body); + invalidate_section (&chooser->nature); + invalidate_section (&chooser->food); + invalidate_section (&chooser->travel); + invalidate_section (&chooser->activities); + invalidate_section (&chooser->objects); + invalidate_section (&chooser->symbols); + invalidate_section (&chooser->flags); + + update_headings (chooser); +} + +static void +setup_section (EGtkEmojiChooser *chooser, + EmojiSection *section, + const char *first, + const char *icon) +{ + GtkAdjustment *adj; + GtkWidget *image; + + section->first = first; + + image = gtk_bin_get_child (GTK_BIN (section->button)); + gtk_image_set_from_icon_name (GTK_IMAGE (image), icon, GTK_ICON_SIZE_BUTTON); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + + gtk_container_set_focus_vadjustment (GTK_CONTAINER (section->box), adj); + gtk_flow_box_set_filter_func (GTK_FLOW_BOX (section->box), filter_func, section, NULL); + g_signal_connect (section->button, "clicked", G_CALLBACK (scroll_to_section), section); +} + +static void +e_gtk_emoji_chooser_add_group (EGtkEmojiChooser *chooser, + GtkBox *emoji_box, + GtkBox *button_box, + const gchar *label, + EmojiSection *section) +{ + section->heading = gtk_label_new (label); + g_object_set (G_OBJECT (section->heading), + "xalign", 0.0, + NULL); + gtk_box_pack_start (emoji_box, GTK_WIDGET (section->heading), FALSE, FALSE, 0); + + section->box = gtk_flow_box_new (); + g_object_set (G_OBJECT (section->box), + "homogeneous", TRUE, + "selection-mode", GTK_SELECTION_NONE, + NULL); + gtk_box_pack_start (emoji_box, GTK_WIDGET (section->box), FALSE, FALSE, 0); + g_signal_connect (section->box, "child-activated", + G_CALLBACK (emoji_activated), chooser); + + section->button = gtk_button_new (); + gtk_container_add (GTK_CONTAINER (section->button), gtk_image_new ()); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (section->button)), "emoji-section"); + g_object_set (G_OBJECT (section->button), + "relief", GTK_RELIEF_NONE, + "tooltip-text", label, + NULL); + gtk_box_pack_start (button_box, GTK_WIDGET (section->button), FALSE, FALSE, 0); +} + +static void +e_gtk_emoji_chooser_construct_without_template (EGtkEmojiChooser *chooser) +{ + GtkBox *box, *emoji_box, *button_box; + GtkGrid *grid; + GtkWidget *widget; + PangoAttrList *attrs; + + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser)), "emoji-picker"); + + box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0)); + gtk_container_add (GTK_CONTAINER (chooser), GTK_WIDGET (box)); + + chooser->search_entry = gtk_search_entry_new (); + gtk_box_pack_start (box, chooser->search_entry, FALSE, FALSE, 0); + + g_signal_connect (chooser->search_entry, "search-changed", G_CALLBACK (search_changed), chooser); + + chooser->stack = gtk_stack_new (); + gtk_box_pack_start (box, chooser->stack, FALSE, FALSE, 0); + + gtk_widget_show_all (GTK_WIDGET (box)); + + box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0)); + gtk_widget_set_name (GTK_WIDGET (box), "list"); + gtk_container_add (GTK_CONTAINER (chooser->stack), GTK_WIDGET (box)); + + chooser->scrolled_window = gtk_scrolled_window_new (NULL, NULL); + g_object_set (G_OBJECT (chooser->scrolled_window), + "vexpand", TRUE, + "hscrollbar-policy", GTK_POLICY_NEVER, + "min-content-height", 250, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser->scrolled_window)), "view"); + gtk_box_pack_start (box, chooser->scrolled_window, FALSE, FALSE, 0); + + emoji_box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0)); + g_object_set (G_OBJECT (emoji_box), + "margin", 6, + "spacing", 6, + NULL); + gtk_container_add (GTK_CONTAINER (chooser->scrolled_window), GTK_WIDGET (emoji_box)); + + button_box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0)); + gtk_box_pack_start (box, GTK_WIDGET (button_box), FALSE, FALSE, 0); + + chooser->recent.box = gtk_flow_box_new (); + g_object_set (G_OBJECT (chooser->recent.box), + "homogeneous", TRUE, + "selection-mode", GTK_SELECTION_NONE, + NULL); + gtk_box_pack_start (emoji_box, GTK_WIDGET (chooser->recent.box), FALSE, FALSE, 0); + g_signal_connect (chooser->recent.box, "child-activated", + G_CALLBACK (emoji_activated), chooser); + + chooser->recent.button = gtk_button_new (); + gtk_container_add (GTK_CONTAINER (chooser->recent.button), gtk_image_new ()); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser->recent.button)), "emoji-section"); + g_object_set (G_OBJECT (chooser->recent.button), + "relief", GTK_RELIEF_NONE, + "tooltip-text", _("Recent"), + NULL); + gtk_box_pack_start (button_box, GTK_WIDGET (chooser->recent.button), FALSE, FALSE, 0); + + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Smileys & People"), &chooser->people); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Body & Clothing"), &chooser->body); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Animals & Nature"), &chooser->nature); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Food & Drink"), &chooser->food); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Travel & Places"), &chooser->travel); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Activities"), &chooser->activities); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("emoji category", "Objects"), &chooser->objects); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Symbols"), &chooser->symbols); + e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Flags"), &chooser->flags); + + grid = GTK_GRID (gtk_grid_new ()); + g_object_set (G_OBJECT (grid), + "name", "empty", + "row-spacing", 12, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (grid)), "dim-label"); + gtk_container_add (GTK_CONTAINER (chooser->stack), GTK_WIDGET (grid)); + + widget = gtk_image_new (); + g_object_set (G_OBJECT (widget), + "icon-name", "edit-find-symbolic", + "pixel-size", 72, + NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (widget), "dim-label"); + gtk_grid_attach (grid, widget, 0, 0, 1, 1); + + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD)); + pango_attr_list_insert (attrs, pango_attr_scale_new (1.44)); + + widget = gtk_label_new (_("No Results Found")); + g_object_set (G_OBJECT (widget), + "attributes", attrs, + NULL); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + pango_attr_list_unref (attrs); + + widget = gtk_label_new (_("Try a different search")); + gtk_style_context_add_class (gtk_widget_get_style_context (widget), "dim-label"); + gtk_grid_attach (grid, widget, 0, 1, 1, 1); + + gtk_widget_show_all (GTK_WIDGET (chooser->stack)); +} + +static void +e_gtk_emoji_chooser_init (EGtkEmojiChooser *chooser) +{ + GtkAdjustment *adj; + + chooser->settings = g_settings_new ("org.gtk.Settings.EmojiChooser"); + + e_gtk_emoji_chooser_construct_without_template (chooser); + + /* Get a reasonable maximum width for an emoji. We do this to + * skip overly wide fallback rendering for certain emojis the + * font does not contain and therefore end up being rendered + * as multiply glyphs. + */ + { + PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET (chooser), "🙂"); + PangoAttrList *attrs; + PangoRectangle rect; + + attrs = pango_attr_list_new (); + pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE)); + pango_layout_set_attributes (layout, attrs); + pango_attr_list_unref (attrs); + + pango_layout_get_extents (layout, &rect, NULL); + chooser->emoji_max_width = rect.width; + + g_object_unref (layout); + } + + chooser->recent_long_press = gtk_gesture_long_press_new (chooser->recent.box); + g_signal_connect (chooser->recent_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + chooser->recent_multi_press = gtk_gesture_multi_press_new (chooser->recent.box); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->recent_multi_press), GDK_BUTTON_SECONDARY); + g_signal_connect (chooser->recent_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser); + + chooser->people_long_press = gtk_gesture_long_press_new (chooser->people.box); + g_signal_connect (chooser->people_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + chooser->people_multi_press = gtk_gesture_multi_press_new (chooser->people.box); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->people_multi_press), GDK_BUTTON_SECONDARY); + g_signal_connect (chooser->people_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser); + + chooser->body_long_press = gtk_gesture_long_press_new (chooser->body.box); + g_signal_connect (chooser->body_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + chooser->body_multi_press = gtk_gesture_multi_press_new (chooser->body.box); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->body_multi_press), GDK_BUTTON_SECONDARY); + g_signal_connect (chooser->body_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + g_signal_connect (adj, "value-changed", G_CALLBACK (adj_value_changed), chooser); + + setup_section (chooser, &chooser->recent, NULL, "emoji-recent-symbolic"); + setup_section (chooser, &chooser->people, "grinning face", "emoji-people-symbolic"); + setup_section (chooser, &chooser->body, "selfie", "emoji-body-symbolic"); + setup_section (chooser, &chooser->nature, "monkey face", "emoji-nature-symbolic"); + setup_section (chooser, &chooser->food, "grapes", "emoji-food-symbolic"); + setup_section (chooser, &chooser->travel, "globe showing Europe-Africa", "emoji-travel-symbolic"); + setup_section (chooser, &chooser->activities, "jack-o-lantern", "emoji-activities-symbolic"); + setup_section (chooser, &chooser->objects, "muted speaker", "emoji-objects-symbolic"); + setup_section (chooser, &chooser->symbols, "ATM sign", "emoji-symbols-symbolic"); + setup_section (chooser, &chooser->flags, "chequered flag", "emoji-flags-symbolic"); + + populate_recent_section (chooser); + + chooser->populate_idle = g_idle_add (populate_emoji_chooser, chooser); + g_source_set_name_by_id (chooser->populate_idle, "[gtk] populate_emoji_chooser"); +} + +static void +e_gtk_emoji_chooser_show (GtkWidget *widget) +{ + EGtkEmojiChooser *chooser = E_GTK_EMOJI_CHOOSER (widget); + GtkAdjustment *adj; + + GTK_WIDGET_CLASS (e_gtk_emoji_chooser_parent_class)->show (widget); + + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); + gtk_adjustment_set_value (adj, 0); + + gtk_entry_set_text (GTK_ENTRY (chooser->search_entry), ""); +} + +static void +e_gtk_emoji_chooser_class_init (EGtkEmojiChooserClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = e_gtk_emoji_chooser_finalize; + widget_class->show = e_gtk_emoji_chooser_show; + + signals[EMOJI_PICKED] = g_signal_new ("emoji-picked", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 1, G_TYPE_STRING|G_SIGNAL_TYPE_STATIC_SCOPE); +} + +GtkWidget * +e_gtk_emoji_chooser_new (void) +{ + return GTK_WIDGET (g_object_new (E_GTK_TYPE_EMOJI_CHOOSER, NULL)); +} diff --git a/src/e-util/e-gtkemojichooser.h b/src/e-util/e-gtkemojichooser.h new file mode 100644 index 0000000000..c7ca4dc70c --- /dev/null +++ b/src/e-util/e-gtkemojichooser.h @@ -0,0 +1,45 @@ +/* + * Copyright 2017, Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/* Copied and adapted a bit from gtk+'s gtkemojichooser.h, + waiting for it to be made public: + https://gitlab.gnome.org/GNOME/gtk/issues/86 +*/ + +#ifndef E_GTKEMOJICHOOSER_H +#define E_GTKEMOJICHOOSER_H + +#include + +G_BEGIN_DECLS + +#define E_GTK_TYPE_EMOJI_CHOOSER (e_gtk_emoji_chooser_get_type ()) +#define E_GTK_EMOJI_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_GTK_TYPE_EMOJI_CHOOSER, EGtkEmojiChooser)) +#define E_GTK_EMOJI_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), E_GTK_TYPE_EMOJI_CHOOSER, EGtkEmojiChooserClass)) +#define E_GTK_IS_EMOJI_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_GTK_TYPE_EMOJI_CHOOSER)) +#define E_GTK_IS_EMOJI_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), E_GTK_TYPE_EMOJI_CHOOSER)) +#define E_GTK_EMOJI_CHOOSER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), E_GTK_TYPE_EMOJI_CHOOSER, EGtkEmojiChooserClass)) + +typedef struct _EGtkEmojiChooser EGtkEmojiChooser; +typedef struct _EGtkEmojiChooserClass EGtkEmojiChooserClass; + +GType e_gtk_emoji_chooser_get_type (void) G_GNUC_CONST; +GtkWidget * e_gtk_emoji_chooser_new (void); + +G_END_DECLS + +#endif /* E_GTKEMOJICHOOSER_H */ diff --git a/src/e-util/e-html-editor-actions.c b/src/e-util/e-html-editor-actions.c index b65e22fbbe..0c151a72a4 100644 --- a/src/e-util/e-html-editor-actions.c +++ b/src/e-util/e-html-editor-actions.c @@ -29,6 +29,7 @@ #include "e-html-editor-actions.h" #include "e-emoticon-action.h" #include "e-emoticon-chooser.h" +#include "e-gtkemojichooser.h" #include "e-image-chooser-dialog.h" #include "e-spell-checker.h" #include "e-misc-utils.h" @@ -274,6 +275,43 @@ action_indent_cb (GtkAction *action, e_content_editor_selection_indent (cnt_editor); } +static void +emoji_chooser_emoji_picked_cb (EHTMLEditor *editor, + const gchar *emoji_text) +{ + if (emoji_text) { + EContentEditor *cnt_editor; + + cnt_editor = e_html_editor_get_content_editor (editor); + + e_content_editor_insert_content (cnt_editor, emoji_text, + E_CONTENT_EDITOR_INSERT_CONVERT | + E_CONTENT_EDITOR_INSERT_TEXT_PLAIN); + } +} + +static void +action_insert_emoji_cb (GtkAction *action, + EHTMLEditor *editor) +{ + if (!editor->priv->emoji_chooser) { + GtkWidget *popover; + + popover = e_gtk_emoji_chooser_new (); + + gtk_popover_set_relative_to (GTK_POPOVER (popover), GTK_WIDGET (editor)); + gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_BOTTOM); + gtk_popover_set_modal (GTK_POPOVER (popover), TRUE); + + g_signal_connect_object (popover, "emoji-picked", + G_CALLBACK (emoji_chooser_emoji_picked_cb), editor, G_CONNECT_SWAPPED); + + editor->priv->emoji_chooser = popover; + } + + gtk_popover_popup (GTK_POPOVER (editor->priv->emoji_chooser)); +} + static void action_insert_emoticon_cb (GtkAction *action, EHTMLEditor *editor) @@ -1015,6 +1053,13 @@ static GtkActionEntry core_editor_entries[] = { N_("Increase Indent"), G_CALLBACK (action_indent_cb) }, + { "insert-emoji", + NULL, + N_("E_moji"), + NULL, + N_("Insert Emoji"), + G_CALLBACK (action_insert_emoji_cb) }, + { "insert-html-file", NULL, N_("_HTML File…"), @@ -2016,6 +2061,9 @@ editor_actions_init (EHTMLEditor *editor) g_object_set ( G_OBJECT (ACTION (SHOW_REPLACE)), "short-label", _("Re_place"), NULL); + g_object_set ( + G_OBJECT (ACTION (INSERT_EMOJI)), + "short-label", _("E_moji"), NULL); g_object_set ( G_OBJECT (ACTION (INSERT_IMAGE)), "short-label", _("_Image"), NULL); diff --git a/src/e-util/e-html-editor-actions.h b/src/e-util/e-html-editor-actions.h index f724db0be2..74b2773eee 100644 --- a/src/e-util/e-html-editor-actions.h +++ b/src/e-util/e-html-editor-actions.h @@ -85,6 +85,8 @@ E_HTML_EDITOR_ACTION ((editor), "format-text") #define E_HTML_EDITOR_ACTION_INSERT_EMOTICON(editor) \ E_HTML_EDITOR_ACTION ((editor), "insert-emoticon") +#define E_HTML_EDITOR_ACTION_INSERT_EMOJI(editor) \ + E_HTML_EDITOR_ACTION ((editor), "insert-emoji") #define E_HTML_EDITOR_ACTION_INSERT_IMAGE(editor) \ E_HTML_EDITOR_ACTION ((editor), "insert-image") #define E_HTML_EDITOR_ACTION_INSERT_LINK(editor) \ diff --git a/src/e-util/e-html-editor-manager.ui b/src/e-util/e-html-editor-manager.ui index 487dce6be0..e6af175d34 100644 --- a/src/e-util/e-html-editor-manager.ui +++ b/src/e-util/e-html-editor-manager.ui @@ -44,6 +44,7 @@ + diff --git a/src/e-util/e-html-editor-private.h b/src/e-util/e-html-editor-private.h index ec068f4ae6..5d66c203b2 100644 --- a/src/e-util/e-html-editor-private.h +++ b/src/e-util/e-html-editor-private.h @@ -83,6 +83,8 @@ struct _EHTMLEditorPrivate { GtkWidget *style_combo_box; GtkWidget *scrolled_window; + GtkWidget *emoji_chooser; + GHashTable *content_editors; EContentEditor *use_content_editor;