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.
This commit is contained in:
Matthias Clasen 2017-08-11 11:54:15 -04:00
parent 44999fa239
commit c6c1619646
9 changed files with 1083 additions and 1 deletions

View File

@ -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@

611
gtk/gtkemojichooser.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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));
}

41
gtk/gtkemojichooser.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gtk/gtk.h> can be included directly."
#endif
#include <gtk/gtkwidget.h>
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

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id='org.gtk.Settings.EmojiChooser' path='/org/gtk/settings/emoji-chooser/'>
<key name='recent-emoji' type='a(ausaau)'>
<default>[]</default>
<summary>Recently used Emoji</summary>
<description>
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.
</description>
</key>
</schema>
</schemalist>

View File

@ -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;
}

View File

@ -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 */

View File

@ -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 */

307
gtk/ui/gtkemojichooser.ui Normal file
View File

@ -0,0 +1,307 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface domain="gtk40">
<template class="GtkEmojiChooser" parent="GtkPopover">
<property name="modal">1</property>
<style>
<class name="emoji-picker"/>
</style>
<child>
<object class="GtkBox" id="box">
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<signal name="search-changed" handler="search_changed"/>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="vexpand">1</property>
<property name="hscrollbar-policy">never</property>
<property name="min-content-height">250</property>
<style>
<class name="view"/>
</style>
<child>
<object class="GtkBox" id="emoji_box">
<property name="orientation">vertical</property>
<property name="margin">6</property>
<property name="spacing">6</property>
<child>
<object class="GtkFlowBox" id="recent.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="people.heading">
<property name="label" translatable="yes" context="emoji category">Smileys &amp; People</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="people.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="body.heading">
<property name="label" translatable="yes" context="emoji category">Body &amp; Clothing</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="body.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="nature.heading">
<property name="label" translatable="yes" context="emoji category">Animals &amp; Nature</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="nature.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="food.heading">
<property name="label" translatable="yes" context="emoji category">Food &amp; Drink</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="food.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="travel.heading">
<property name="label" translatable="yes" context="emoji category">Travel &amp; Places</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="travel.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="activities.heading">
<property name="label" translatable="yes" context="emoji category">Activities</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="activities.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="objects.heading">
<property name="label" translatable="yes" context="emoji category">Objects</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="objects.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="symbols.heading">
<property name="label" translatable="yes" context="emoji category">Symbols</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="symbols.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
<child>
<object class="GtkLabel" id="flags.heading">
<property name="label" translatable="yes" context="emoji category">Flags</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="flags.box">
<property name="homogeneous">1</property>
<property name="selection-mode">none</property>
<signal name="child-activated" handler="emoji_activated"/>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="GtkButton" id="recent.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="people.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="body.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="nature.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="food.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="travel.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="activities.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="objects.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="symbols.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="flags.button">
<property name="relief">none</property>
<style>
<class name="emoji-section"/>
</style>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">list</property>
</packing>
</child>
<child>
<object class="GtkGrid">
<property name="row-spacing">12</property>
<property name="halign">center</property>
<property name="valign">center</property>
<style>
<class name="dim-label"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">edit-find-symbolic</property>
<property name="pixel-size">72</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">No Results Found</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.44"/>
</attributes>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Try a different search</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">empty</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -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