[polari/wip/fmuellner/emoji-picker: 4/6] emojiPicker: Add a simple emoji picker



commit 77b94e031467c329d741975df6314289c63d6248
Author: Florian Müllner <fmuellner gnome org>
Date:   Tue Aug 8 03:01:54 2017 +0200

    emojiPicker: Add a simple emoji picker
    
    Rejoice, pango, fontconfig and cairo now support color emojis. GTK+
    should gain an emoji picker eventually, but in order to enjoy the
    colorful goodness right away, implement a simple picker ourselves
    in the meantime.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=755579

 data/resources/application.css         |   13 ++
 src/emojiPicker.js                     |  236 ++++++++++++++++++++++++++++++++
 src/org.gnome.Polari.src.gresource.xml |    1 +
 3 files changed, 250 insertions(+), 0 deletions(-)
---
diff --git a/data/resources/application.css b/data/resources/application.css
index c75a2ea..c0f7a38 100644
--- a/data/resources/application.css
+++ b/data/resources/application.css
@@ -106,6 +106,19 @@ treeview.polari-server-room-list {
     padding: 6px;
 }
 
+.emoji {
+    font-size: xx-large;
+    padding: 8px;
+    border-radius: 8px;
+}
+.emoji:hover { background: @theme_selected_bg_color; }
+
+.emoji-section { padding: 4px; }
+.emoji-section:checked { border-color: transparent; background: transparent; }
+
+.emoji-section label { border-bottom: 2px solid transparent; padding: 0 12px; }
+.emoji-section:checked label { border-bottom-color: @theme_selected_bg_color; }
+
 /* Hack: Move separator by 1px so that it aligns with the sidebar */
 .titlebar > separator:dir(ltr) { margin-left: -1px; }
 .titlebar > headerbar:first-child:dir(ltr) { margin-right: 1px; }
diff --git a/src/emojiPicker.js b/src/emojiPicker.js
new file mode 100644
index 0000000..2116965
--- /dev/null
+++ b/src/emojiPicker.js
@@ -0,0 +1,236 @@
+const Gdk = imports.gi.Gdk;
+const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const modifierBlacklist = [
+    'child',
+    'adult',
+    'older adult',
+    'woman with headscarf',
+    'bearded person',
+    'breast-feeding',
+    'mage',
+    'fairy',
+    'vampire',
+    'merperson',
+    'merman',
+    'mermaid',
+    'elf',
+    'genie',
+    'zombie',
+    'in steamy room',
+    'climbing',
+    'in lotus position',
+    'person in bed',
+    'man in suit levitating',
+    'horse racing',
+    'snowboarder',
+    'golfing',
+    'love-you gesture',
+    'palms up together',
+];
+
+let _emojis = null;
+
+function getEmojis() {
+    if (_emojis != null)
+        return _emojis;
+
+    let uri = 'resource:///org/gnome/Polari/data/emoji.json';
+    let file = Gio.file_new_for_uri(uri);
+    try {
+        let [success, data] = file.load_contents(null);
+        _emojis = JSON.parse(data).filter(e => {
+            if (e.name == 'world map')
+                return false; // too wide
+
+            if (!e.code.includes(' '))
+                return true; // no modifiers
+
+            // FIXME: Figure out programmatically where modifiers
+            // don't work (yet) instead of relying on a blacklist
+            return !modifierBlacklist.some(n => e.name.includes(n));
+        });
+    } catch(e) {
+        log('Failed to load emoji definitions: ' + e.message);
+        _emojis = [];
+    }
+
+    return _emojis;
+}
+
+const Emoji = new Lang.Class({
+    Name: 'Emoji',
+    Extends: Gtk.FlowBoxChild,
+
+    _init: function(emojiData) {
+        this._name = emojiData.name;
+        this._matchName = this._name.toLowerCase();
+        this._char = emojiData.char;
+
+        this.parent();
+
+        this.get_style_context().add_class('emoji');
+
+        let box = new Gtk.EventBox();
+        box.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK |
+                       Gdk.EventMask.LEAVE_NOTIFY_MASK);
+        this.add(box);
+
+        box.connect('enter-notify-event', () => {
+            this.set_state_flags(Gtk.StateFlags.PRELIGHT, false);
+        });
+        box.connect('leave-notify-event', () => {
+            let state = this.get_state_flags();
+            this.unset_state_flags(Gtk.StateFlags.PRELIGHT);
+        });
+
+        box.add(new Gtk.Label({ label: this._char }));
+        box.show_all();
+    },
+
+    match: function(terms) {
+        return terms.every(t => this._matchName.includes(t));
+    },
+
+    get emoji() {
+        return this._char;
+    }
+});
+
+const SectionIndicator = new Lang.Class({
+    Name: 'SectionIndicator',
+    Extends: Gtk.Button,
+
+    _init: function(labelCode, from, to) {
+        this._from = from;
+        this._to = to;
+
+        this.parent({ relief: Gtk.ReliefStyle.NONE });
+
+        this.get_style_context().add_class('emoji-section');
+
+        this.add(new Gtk.Label({ label: String.fromCodePoint(labelCode, 0xfe0e),
+                                 visible: true }));
+    },
+
+    updateForIndex: function(index) {
+        if (this._from <= index && index <= this._to)
+            this.set_state_flags(Gtk.StateFlags.CHECKED, false);
+        else
+            this.unset_state_flags(Gtk.StateFlags.CHECKED);
+    }
+});
+
+let sections = {
+    people:  { labelCode: 0x1f642,
+               fromNo: 1, toNo: 1232 },
+    body:    { labelCode: 0x1f44d,
+               fromNo: 1238, toNo: 1507 },
+    nature:  { labelCode: 0x1f33c,
+               fromNo: 1508, toNo: 1620 },
+    food:    { labelCode: 0x1f374,
+               fromNo: 1621, toNo: 1722 },
+    travel:  { labelCode: 0x1f698,
+               fromNo: 1723, toNo: 1846 },
+    symbols: { labelCode: 0x2665,
+               fromNo: 1847, toNo: 2356 },
+    flags:   { labelCode: 0x1f3f4,
+               fromNo: 2357, toNo: 2623 }
+};
+
+var EmojiPicker = new Lang.Class({
+    Name: 'EmojiPicker',
+    Extends: Gtk.Popover,
+    Signals: { 'emoji-picked': { param_types: [GObject.TYPE_STRING] } },
+
+    _init: function(params) {
+        this._terms = [];
+
+        this.parent(params);
+
+        let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL });
+        this.add(box);
+
+        let entry = new Gtk.SearchEntry({ margin: 8 });
+        box.add(entry);
+
+        let scrolled = new Gtk.ScrolledWindow({ min_content_height: 250 });
+        scrolled.hscrollbar_policy = Gtk.PolicyType.NEVER;
+        this._adjustment = scrolled.vadjustment;
+        box.add(scrolled);
+
+        this._flowBox = new Gtk.FlowBox({ max_children_per_line: 6,
+                                          min_children_per_line: 6,
+                                          border_width: 8,
+                                          selection_mode: Gtk.SelectionMode.NONE,
+                                          halign: Gtk.Align.START,
+                                          valign: Gtk.Align.START });
+        scrolled.add(this._flowBox);
+
+        getEmojis().forEach(e => {
+            let emoji = new Emoji(e);
+            this._flowBox.add(emoji);
+
+            for (let name in sections) {
+                if (e.no == sections[name].fromNo)
+                    sections[name].from = emoji.get_index();
+                else if (e.no == sections[name].toNo)
+                    sections[name].to = emoji.get_index();
+            }
+        });
+
+        this._sectionBox = new Gtk.Box();
+        for (let name in sections) {
+            let { labelCode, to, from } = sections[name];
+            let section = new SectionIndicator(labelCode, from, to);
+            section.connect('clicked', () => {
+                let child = this._flowBox.get_child_at_index(from);
+                let alloc = child.get_allocation();
+                this._adjustment.value = alloc.y;
+            });
+
+            this._sectionBox.add(section);
+        }
+        box.add(this._sectionBox);
+
+        box.show_all();
+
+        this._flowBox.set_filter_func(c => c.match(this._terms));
+
+        this._flowBox.connect('child-activated', (box, child) => {
+            this.popdown();
+            this.emit('emoji-picked', child.emoji);
+        });
+
+        entry.connect('search-changed', () => {
+            let trimmedText = entry.text.toLowerCase().trim();
+            if (trimmedText)
+                this._terms = trimmedText.split(' ');
+            else
+                this._terms = [];
+            this._flowBox.invalidate_filter();
+            this._updateIndicators();
+        });
+
+        this._adjustment.connect('value-changed',
+                                 Lang.bind(this, this._updateIndicators));
+
+        this.connect('map', () => {
+            entry.text = '';
+            this._adjustment.value = 0;
+            this._updateIndicators();
+        });
+    },
+
+    _updateIndicators: function() {
+        let child = null;
+
+        if (this._terms.length == 0)
+            child = this._flowBox.get_child_at_pos(0, this._adjustment.value);
+        let i = child ? child.get_index() : -1;
+        this._sectionBox.get_children().forEach(c => { c.updateForIndex(i); });
+    }
+});
diff --git a/src/org.gnome.Polari.src.gresource.xml b/src/org.gnome.Polari.src.gresource.xml
index a526f25..80907f0 100644
--- a/src/org.gnome.Polari.src.gresource.xml
+++ b/src/org.gnome.Polari.src.gresource.xml
@@ -6,6 +6,7 @@
     <file>appNotifications.js</file>
     <file>chatView.js</file>
     <file>connections.js</file>
+    <file>emojiPicker.js</file>
     <file>entryArea.js</file>
     <file>ircParser.js</file>
     <file>joinDialog.js</file>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]