From c6c1619646ce62a87a89d54c037c2168e0d9d0d3 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Fri, 11 Aug 2017 11:54:15 -0400 Subject: [PATCH] Add an Emoji chooser widget Add a popover that shows color Emoji, with a search entry. The recently-used Emoji are stored in a GSetting. --- gtk/Makefile.am | 6 +- gtk/gtkemojichooser.c | 611 ++++++++++++++++++ gtk/gtkemojichooser.h | 41 ++ gtk/org.gtk.Settings.EmojiChooser.gschema.xml | 16 + gtk/theme/Adwaita/_common.scss | 49 ++ gtk/theme/Adwaita/gtk-contained-dark.css | 26 + gtk/theme/Adwaita/gtk-contained.css | 26 + gtk/ui/gtkemojichooser.ui | 307 +++++++++ po/POTFILES.in | 2 + 9 files changed, 1083 insertions(+), 1 deletion(-) create mode 100644 gtk/gtkemojichooser.c create mode 100644 gtk/gtkemojichooser.h create mode 100644 gtk/org.gtk.Settings.EmojiChooser.gschema.xml create mode 100644 gtk/ui/gtkemojichooser.ui diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 16fbd183d4..75889b3e5a 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -451,6 +451,7 @@ gtk_private_h_sources = \ gtkdebugupdatesprivate.h \ gtkdialogprivate.h \ gtkdndprivate.h \ + gtkemojichooser.h \ gtkentryprivate.h \ gtkeventcontrollerprivate.h \ gtkfilechooserembed.h \ @@ -722,6 +723,7 @@ gtk_base_c_sources = \ gtkdragsource.c \ gtkdrawingarea.c \ gtkeditable.c \ + gtkemojichooser.c \ gtkentry.c \ gtkentrybuffer.c \ gtkentrycompletion.c \ @@ -1147,7 +1149,8 @@ templates = \ ui/gtkcolorchooserdialog.ui \ ui/gtkcoloreditor.ui \ ui/gtkcombobox.ui \ - ui/gtkdialog.ui \ + ui/gtkdialog.ui \ + ui/gtkemojichooser.ui \ ui/gtkfilechooserwidget.ui \ ui/gtkfilechooserdialog.ui \ ui/gtkfontchooserdialog.ui \ @@ -1690,6 +1693,7 @@ files: gsettings_SCHEMAS = \ org.gtk.Settings.FileChooser.gschema.xml \ org.gtk.Settings.ColorChooser.gschema.xml \ + org.gtk.Settings.EmojiChooser.gschema.xml \ org.gtk.Settings.Debug.gschema.xml @GSETTINGS_RULES@ diff --git a/gtk/gtkemojichooser.c b/gtk/gtkemojichooser.c new file mode 100644 index 0000000000..a26e7dc7fe --- /dev/null +++ b/gtk/gtkemojichooser.c @@ -0,0 +1,611 @@ +/* gtkemojichooser.c: An Emoji chooser widget + * 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 "config.h" + +#include "gtkemojichooser.h" + +#include "gtkadjustment.h" +#include "gtkbox.h" +#include "gtkbutton.h" +#include "gtkcssprovider.h" +#include "gtkentry.h" +#include "gtkflowbox.h" +#include "gtkstack.h" +#include "gtklabel.h" +#include "gtkgesturelongpress.h" +#include "gtkpopover.h" +#include "gtkscrolledwindow.h" +#include "gtkintl.h" +#include "gtkprivate.h" + +typedef struct { + GtkWidget *box; + GtkWidget *heading; + GtkWidget *button; + const char *first; + gunichar label; + gboolean empty; +} EmojiSection; + +struct _GtkEmojiChooser +{ + GtkPopover parent_instance; + + GtkWidget *search_entry; + GtkWidget *stack; + GtkWidget *scrolled_window; + + EmojiSection recent; + EmojiSection people; + EmojiSection body; + EmojiSection nature; + EmojiSection food; + EmojiSection travel; + EmojiSection activities; + EmojiSection objects; + EmojiSection symbols; + EmojiSection flags; + + GtkGesture *recent_press; + GtkGesture *people_press; + GtkGesture *body_press; + + GVariant *data; + + GSettings *settings; +}; + +struct _GtkEmojiChooserClass { + GtkPopoverClass parent_class; +}; + +enum { + EMOJI_PICKED, + LAST_SIGNAL +}; + +static int signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (GtkEmojiChooser, gtk_emoji_chooser, GTK_TYPE_POPOVER) + +static void +gtk_emoji_chooser_finalize (GObject *object) +{ + GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (object); + + g_variant_unref (chooser->data); + g_object_unref (chooser->settings); + + G_OBJECT_CLASS (gtk_emoji_chooser_parent_class)->finalize (object); +} + +static void +scroll_to_section (GtkButton *button, + gpointer data) +{ + EmojiSection *section = data; + GtkEmojiChooser *chooser; + GtkAdjustment *adj; + GtkAllocation alloc = { 0, 0, 0, 0 }; + double page_increment, value; + gboolean dummy; + + chooser = GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (button), 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); + page_increment = gtk_adjustment_get_page_increment (adj); + value = gtk_adjustment_get_value (adj); + gtk_adjustment_set_page_increment (adj, alloc.y - value); + g_signal_emit_by_name (chooser->scrolled_window, "scroll-child", GTK_SCROLL_PAGE_FORWARD, FALSE, &dummy); + gtk_adjustment_set_page_increment (adj, page_increment); +} + +static void +add_emoji (GtkWidget *box, + gboolean prepend, + GVariantIter *iter, + GVariant *data); + +#define MAX_RECENT (7*3) + +static void +add_recent_item (GtkEmojiChooser *chooser, + GVariant *item) +{ + GList *children, *l; + GVariantIter *codes; + const char *name; + int i; + GVariantBuilder builder; + + g_variant_ref (item); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ausaau)")); + g_variant_builder_add_value (&builder, item); + + g_variant_get_child (item, 1, "&s", &name); + + 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"); + const char *name2; + + g_variant_get_child (item2, 1, "&s", &name2); + if (strcmp (name, name2) == 0) + { + 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_value (&builder, item2); + } + g_list_free (children); + + g_variant_get_child (item, 0, "au", &codes); + add_emoji (chooser->recent.box, TRUE, codes, item); + g_variant_iter_free (codes); + + 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) +{ + GtkEmojiChooser *chooser = data; + char *text; + GtkWidget *label; + GVariant *item; + + gtk_popover_popdown (GTK_POPOVER (chooser)); + + label = gtk_bin_get_child (GTK_BIN (child)); + text = g_strdup (gtk_label_get_label (GTK_LABEL (label))); + + item = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data"); + add_recent_item (chooser, item); + + g_signal_emit (data, signals[EMOJI_PICKED], 0, text); + g_free (text); +} + +static void +long_pressed_cb (GtkGesture *gesture, + double x, + double y, + gpointer data) +{ + GtkWidget *child; + GtkWidget *popover; + GtkWidget *view; + GtkWidget *box; + GVariant *emoji_data; + GtkWidget *parent_popover; + GVariantIter *iter; + GVariantIter *codes; + + 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)); + if (!child) + return; + + emoji_data = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data"); + if (!emoji_data) + return; + + g_variant_get_child (emoji_data, 2, "aau", &iter); + if (g_variant_iter_n_children (iter) == 0) + { + g_variant_iter_free (iter); + 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); + + g_variant_get_child (emoji_data, 0, "au", &codes); + add_emoji (box, FALSE, codes, emoji_data); + g_variant_iter_free (codes); + while (g_variant_iter_next (iter, "au", &codes)) + { + add_emoji (box, FALSE, codes, emoji_data); + g_variant_iter_free (codes); + } + + gtk_popover_popup (GTK_POPOVER (popover)); +} + +static void +add_emoji (GtkWidget *box, + gboolean prepend, + GVariantIter *iter, + GVariant *data) +{ + GtkWidget *child; + GtkWidget *label; + PangoAttrList *attrs; + char text[64]; + char *p = text; + gunichar code; + + while (g_variant_iter_next (iter, "u", &code)) + p += g_unichar_to_utf8 (code, p); + 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); + + 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 (data), + (GDestroyNotify)g_variant_unref); + + gtk_container_add (GTK_CONTAINER (child), label); + gtk_flow_box_insert (GTK_FLOW_BOX (box), child, prepend ? 0 : -1); +} + +static void +populate_emoji_chooser (GtkEmojiChooser *chooser) +{ + g_autoptr(GBytes) bytes = NULL; + GVariantIter iter; + GVariant *item; + GtkWidget *box; + + 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(ausaau)"), bytes, TRUE)); + + g_variant_iter_init (&iter, chooser->data); + box = chooser->people.box; + while ((item = g_variant_iter_next_value (&iter))) + { + GVariantIter *codes; + const char *name; + + g_variant_get_child (item, 0, "au", &codes); + g_variant_get_child (item, 1, "&s", &name); + + if (strcmp (name, chooser->body.first) == 0) + box = chooser->body.box; + else if (strcmp (name, chooser->nature.first) == 0) + box = chooser->nature.box; + else if (strcmp (name, chooser->food.first) == 0) + box = chooser->food.box; + else if (strcmp (name, chooser->travel.first) == 0) + box = chooser->travel.box; + else if (strcmp (name, chooser->activities.first) == 0) + box = chooser->activities.box; + else if (strcmp (name, chooser->objects.first) == 0) + box = chooser->objects.box; + else if (strcmp (name, chooser->symbols.first) == 0) + box = chooser->symbols.box; + else if (strcmp (name, chooser->flags.first) == 0) + box = chooser->flags.box; + + add_emoji (box, FALSE, codes, item); + g_variant_iter_free (codes); + } +} + +static void +update_state (EmojiSection *section, + double value) +{ + GtkAllocation alloc = { 0, 0, 0, 20 }; + + if (section->heading) + gtk_widget_get_allocation (section->heading, &alloc); + + if (alloc.y <= value && value < alloc.y + alloc.height) + 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 void +adj_value_changed (GtkAdjustment *adj, + gpointer data) +{ + GtkEmojiChooser *chooser = data; + double value = gtk_adjustment_get_value (adj); + + update_state (&chooser->recent, value); + update_state (&chooser->people, value); + update_state (&chooser->body, value); + update_state (&chooser->nature, value); + update_state (&chooser->food, value); + update_state (&chooser->travel, value); + update_state (&chooser->activities, value); + update_state (&chooser->objects, value); + update_state (&chooser->symbols, value); + update_state (&chooser->flags, value); +} + +static gboolean +filter_func (GtkFlowBoxChild *child, + gpointer data) +{ + EmojiSection *section = data; + GtkEmojiChooser *chooser; + GVariant *emoji_data; + const char *text; + const char *name; + gboolean res; + + res = TRUE; + + chooser = GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), 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 || text[1] == 0) + goto out; + + if (!emoji_data) + goto out; + + g_variant_get_child (emoji_data, 1, "&s", &name); + res = strstr (name, text) != NULL; + +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 gboolean +update_headings (gpointer data) +{ + GtkEmojiChooser *chooser = data; + + gtk_widget_set_visible (chooser->people.heading, !chooser->people.empty); + gtk_widget_set_visible (chooser->body.heading, !chooser->body.empty); + gtk_widget_set_visible (chooser->nature.heading, !chooser->nature.empty); + gtk_widget_set_visible (chooser->food.heading, !chooser->food.empty); + gtk_widget_set_visible (chooser->travel.heading, !chooser->travel.empty); + gtk_widget_set_visible (chooser->activities.heading, !chooser->activities.empty); + gtk_widget_set_visible (chooser->objects.heading, !chooser->objects.empty); + gtk_widget_set_visible (chooser->symbols.heading, !chooser->symbols.empty); + gtk_widget_set_visible (chooser->flags.heading, !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"); + + return G_SOURCE_REMOVE; +} + +static void +search_changed (GtkEntry *entry, + gpointer data) +{ + GtkEmojiChooser *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); + + g_idle_add (update_headings, data); +} + +static void +setup_section (GtkEmojiChooser *chooser, + EmojiSection *section, + const char *first, + gunichar label) +{ + char text[14]; + char *p; + GtkAdjustment *adj; + + section->first = first; + + p = text; + p += g_unichar_to_utf8 (label, p); + p += g_unichar_to_utf8 (0xfe0e, p); + p[0] = 0; + gtk_button_set_label (GTK_BUTTON (section->button), text); + + 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 +gtk_emoji_chooser_init (GtkEmojiChooser *chooser) +{ + GtkAdjustment *adj; + GVariant *variant; + GVariantIter iter; + GVariant *item; + + gtk_widget_init_template (GTK_WIDGET (chooser)); + + chooser->recent_press = gtk_gesture_long_press_new (chooser->recent.box); + g_signal_connect (chooser->recent_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + + chooser->people_press = gtk_gesture_long_press_new (chooser->people.box); + g_signal_connect (chooser->people_press, "pressed", G_CALLBACK (long_pressed_cb), chooser); + + chooser->body_press = gtk_gesture_long_press_new (chooser->body.box); + g_signal_connect (chooser->body_press, "pressed", G_CALLBACK (long_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, 0x1f557); + setup_section (chooser, &chooser->people, "grinning face", 0x1f642); + setup_section (chooser, &chooser->body, "selfie", 0x1f44d); + setup_section (chooser, &chooser->nature, "monkey face", 0x1f337); + setup_section (chooser, &chooser->food, "grapes", 0x1f374); + setup_section (chooser, &chooser->travel, "globe showing Europe-Africa", 0x2708); + setup_section (chooser, &chooser->activities, "jack-o-lantern", 0x1f3c3); + setup_section (chooser, &chooser->objects, "muted speaker", 0x1f514); + setup_section (chooser, &chooser->symbols, "ATM sign", 0x2764); + setup_section (chooser, &chooser->flags, "chequered flag", 0x1f3f4); + + populate_emoji_chooser (chooser); + + chooser->settings = g_settings_new ("org.gtk.Settings.EmojiChooser"); + variant = g_settings_get_value (chooser->settings, "recent-emoji"); + g_variant_iter_init (&iter, variant); + while ((item = g_variant_iter_next_value (&iter))) + { + GVariantIter *codes; + + g_variant_get_child (item, 0, "au", &codes); + add_emoji (chooser->recent.box, FALSE, codes, item); + g_variant_iter_free (codes); + } +} + +static void +gtk_emoji_chooser_show (GtkWidget *widget) +{ + GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (widget); + GtkAdjustment *adj; + + 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), ""); + + GTK_WIDGET_CLASS (gtk_emoji_chooser_parent_class)->show (widget); +} + +static void +gtk_emoji_chooser_class_init (GtkEmojiChooserClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtk_emoji_chooser_finalize; + widget_class->show = 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); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkemojichooser.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, search_entry); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, stack); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, scrolled_window); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, recent.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, recent.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.button); + + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.box); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.heading); + gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.button); + + gtk_widget_class_bind_template_callback (widget_class, emoji_activated); + gtk_widget_class_bind_template_callback (widget_class, search_changed); +} + +GtkWidget * +gtk_emoji_chooser_new (void) +{ + return GTK_WIDGET (g_object_new (GTK_TYPE_EMOJI_CHOOSER, NULL)); +} diff --git a/gtk/gtkemojichooser.h b/gtk/gtkemojichooser.h new file mode 100644 index 0000000000..63670b5d5c --- /dev/null +++ b/gtk/gtkemojichooser.h @@ -0,0 +1,41 @@ +/* gtkemojichooser.h: An Emoji chooser widget + * 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 . + */ + +#pragma once + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_EMOJI_CHOOSER (gtk_emoji_chooser_get_type ()) +#define GTK_EMOJI_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_EMOJI_CHOOSER, GtkEmojiChooser)) +#define GTK_EMOJI_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_EMOJI_CHOOSER, GtkEmojiChooserClass)) +#define GTK_IS_EMOJI_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_EMOJI_CHOOSER)) +#define GTK_IS_EMOJI_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_EMOJI_CHOOSER)) +#define GTK_EMOJI_CHOOSER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_EMOJI_CHOOSER, GtkEmojiChooserClass)) + +typedef struct _GtkEmojiChooser GtkEmojiChooser; +typedef struct _GtkEmojiChooserClass GtkEmojiChooserClass; + +GType gtk_emoji_chooser_get_type (void) G_GNUC_CONST; +GtkWidget *gtk_emoji_chooser_new (void); + +G_END_DECLS diff --git a/gtk/org.gtk.Settings.EmojiChooser.gschema.xml b/gtk/org.gtk.Settings.EmojiChooser.gschema.xml new file mode 100644 index 0000000000..d72027566d --- /dev/null +++ b/gtk/org.gtk.Settings.EmojiChooser.gschema.xml @@ -0,0 +1,16 @@ + + + + + + [] + Recently used Emoji + + An array of Emoji definitions to show in the Emoji chooser. Each Emoji is + specified as an array of codepoints, a name, and an optional array of + nested Emoji. + + + + + diff --git a/gtk/theme/Adwaita/_common.scss b/gtk/theme/Adwaita/_common.scss index 722ceae082..8f7c006e5d 100644 --- a/gtk/theme/Adwaita/_common.scss +++ b/gtk/theme/Adwaita/_common.scss @@ -4370,3 +4370,52 @@ stackswitcher button.text-button.circular { // FIXME aggregate with buttons min-height: 32px; padding: 0; } + +popover.emoji-picker { padding-left: 0; padding-right: 0; } + +button.emoji-section { + border-color: transparent; + border-width: 3px; + border-style: none none solid; + border-radius: 0; + + margin: 2px 4px 2px 4px; + padding: 3px 0 0; + min-width: 32px; + min-height: 28px; + + /* reset props inherited from the button style */ + background: none; + box-shadow: none; + text-shadow: none; + + outline-offset: -5px; +} + +button.emoji-section label { padding: 0; } + +button.emoji-section:hover { border-color: $borders_color; } + +button.emoji-section:checked { border-color: $selected_bg_color; } + +button.emoji-section:backdrop:not(:checked) { border-color: transparent; } + +button.emoji-section { color: $borders_color; } + +button.emoji-section:hover { color: mix($fg_color, $borders_color, 0.5); } + +button.emoji-section:checked { color: $fg_color; } + +button.emoji-section:backdrop { color: $backdrop_borders_color; } + +button.emoji-section:backdrop:checked { color: $backdrop_fg_color; } + +.emoji { + font-size: x-large; + padding: 6px; + border-radius: 6px; +} + +.emoji:hover { + background: $selected_bg_color; +} diff --git a/gtk/theme/Adwaita/gtk-contained-dark.css b/gtk/theme/Adwaita/gtk-contained-dark.css index d8def7f1f9..833518ec4f 100644 --- a/gtk/theme/Adwaita/gtk-contained-dark.css +++ b/gtk/theme/Adwaita/gtk-contained-dark.css @@ -1893,6 +1893,32 @@ stackswitcher button.text-button { min-width: 100px; } stackswitcher button.circular, stackswitcher button.text-button.circular { min-width: 32px; min-height: 32px; padding: 0; } +popover.emoji-picker { padding-left: 0; padding-right: 0; } + +button.emoji-section { border-color: transparent; border-width: 3px; border-style: none none solid; border-radius: 0; margin: 2px 4px 2px 4px; padding: 3px 0 0; min-width: 32px; min-height: 28px; /* reset props inherited from the button style */ background: none; box-shadow: none; text-shadow: none; outline-offset: -5px; } + +button.emoji-section label { padding: 0; } + +button.emoji-section:hover { border-color: #1b1f20; } + +button.emoji-section:checked { border-color: #215d9c; } + +button.emoji-section:backdrop:not(:checked) { border-color: transparent; } + +button.emoji-section { color: #1b1f20; } + +button.emoji-section:hover { color: #1c2021; } + +button.emoji-section:checked { color: #eeeeec; } + +button.emoji-section:backdrop { color: #202425; } + +button.emoji-section:backdrop:checked { color: #919494; } + +.emoji { font-size: x-large; padding: 6px; border-radius: 6px; } + +.emoji:hover { background: #215d9c; } + /* GTK NAMED COLORS ---------------- use responsibly! */ /* widget text/foreground color */ diff --git a/gtk/theme/Adwaita/gtk-contained.css b/gtk/theme/Adwaita/gtk-contained.css index 6d53c71a42..9829f5afdc 100644 --- a/gtk/theme/Adwaita/gtk-contained.css +++ b/gtk/theme/Adwaita/gtk-contained.css @@ -1913,6 +1913,32 @@ stackswitcher button.text-button { min-width: 100px; } stackswitcher button.circular, stackswitcher button.text-button.circular { min-width: 32px; min-height: 32px; padding: 0; } +popover.emoji-picker { padding-left: 0; padding-right: 0; } + +button.emoji-section { border-color: transparent; border-width: 3px; border-style: none none solid; border-radius: 0; margin: 2px 4px 2px 4px; padding: 3px 0 0; min-width: 32px; min-height: 28px; /* reset props inherited from the button style */ background: none; box-shadow: none; text-shadow: none; outline-offset: -5px; } + +button.emoji-section label { padding: 0; } + +button.emoji-section:hover { border-color: #b6b6b3; } + +button.emoji-section:checked { border-color: #4a90d9; } + +button.emoji-section:backdrop:not(:checked) { border-color: transparent; } + +button.emoji-section { color: #b6b6b3; } + +button.emoji-section:hover { color: #b5b5b2; } + +button.emoji-section:checked { color: #2e3436; } + +button.emoji-section:backdrop { color: #c0c0bd; } + +button.emoji-section:backdrop:checked { color: #8b8e8f; } + +.emoji { font-size: x-large; padding: 6px; border-radius: 6px; } + +.emoji:hover { background: #4a90d9; } + /* GTK NAMED COLORS ---------------- use responsibly! */ /* widget text/foreground color */ diff --git a/gtk/ui/gtkemojichooser.ui b/gtk/ui/gtkemojichooser.ui new file mode 100644 index 0000000000..cbf851b825 --- /dev/null +++ b/gtk/ui/gtkemojichooser.ui @@ -0,0 +1,307 @@ + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index 6c4dc5ae6b..05b4227732 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -114,6 +114,7 @@ gtk/gtkdragdest.c gtk/gtkdragsource.c gtk/gtkdrawingarea.c gtk/gtkeditable.c +gtk/gtkemojichooser.c gtk/gtkentrybuffer.c gtk/gtkentry.c gtk/gtkentrycompletion.c @@ -329,6 +330,7 @@ gtk/ui/gtkassistant.ui gtk/ui/gtkcolorchooserdialog.ui gtk/ui/gtkcoloreditor.ui gtk/ui/gtkdialog.ui +gtk/ui/gtkemojichooser.ui gtk/ui/gtkfilechooserdialog.ui gtk/ui/gtkfilechooserwidget.ui gtk/ui/gtkfontchooserdialog.ui