Commit 5feb4bec authored by Philipp Hörist's avatar Philipp Hörist
Browse files

Rework Emoji implementation

- Use emoji data from a generated dict based on the offical unicode docs,
this makes it easier to update in the future
- Rewrite the emoji chooser
- Add a search field to the emoji chooser
- The emoji chooser is loaded async
- Update to current Unicode 11 Noto theme
parent e37ab6b5
......@@ -42,7 +42,7 @@
from gajim import notify
import re
from gajim import emoticons
from gajim.gtk.emoji_chooser import emoji_chooser
from gajim.common import events
from gajim.common import app
from gajim.common import helpers
......@@ -676,7 +676,7 @@ def _on_message_textview_key_press_event(self, widget, event):
event.keyval == Gdk.KEY_KP_Enter: # ENTER
message_textview = widget
message_buffer = message_textview.get_buffer()
emoticons.replace_with_codepoint(message_buffer)
message_textview.replace_emojis()
start_iter, end_iter = message_buffer.get_bounds()
message = message_buffer.get_text(start_iter, end_iter, False)
xhtml = self.msg_textview.get_xhtml()
......@@ -1055,10 +1055,9 @@ def set_emoticon_popover(self):
if not self.parent_win:
return
popover = emoticons.get_popover()
popover.set_callbacks(self.msg_textview)
emoji_chooser.text_widget = self.msg_textview
emoticons_button = self.xml.get_object('emoticons_button')
emoticons_button.set_popover(popover)
emoticons_button.set_popover(emoji_chooser)
def on_color_menuitem_activate(self, widget):
color_dialog = Gtk.ColorChooserDialog(None, self.parent_win.window)
......
......@@ -129,6 +129,7 @@ def __init__(self):
source_paths = [
('DATA', os.path.join(basedir, 'data')),
('STYLE', os.path.join(basedir, 'data', 'style')),
('EMOTICONS', os.path.join(basedir, 'data', 'emoticons')),
('GUI', os.path.join(basedir, 'data', 'gui')),
('ICONS', os.path.join(basedir, 'data', 'icons')),
('HOME', os.path.expanduser('~')),
......
......@@ -1479,36 +1479,29 @@ def version_condition(current_version, required_version):
def get_available_emoticon_themes():
emoticons_themes = []
emoticons_data_path = os.path.join(configpaths.get('DATA'), 'emoticons')
font_theme_path = os.path.join(
configpaths.get('DATA'), 'emoticons', 'font-emoticons', 'emoticons_theme.py')
if sys.platform not in ('win32', 'darwin'):
# Colored emoji fonts only supported on Linux
emoticons_themes.append('font')
files = []
with os.scandir(configpaths.get('EMOTICONS')) as scan:
for entry in scan:
if not entry.is_dir():
continue
with os.scandir(entry.path) as scan_theme:
for theme in scan_theme:
if theme.is_file():
files.append(theme.name)
folders = os.listdir(emoticons_data_path)
if os.path.isdir(configpaths.get('MY_EMOTS')):
folders += os.listdir(configpaths.get('MY_EMOTS'))
file = 'emoticons_theme.py'
if os.name == 'nt' and not os.path.exists(font_theme_path):
# When starting Gajim from source .py files are available
# We test this with font-emoticons and fallback to .pyc files otherwise
file = 'emoticons_theme.pyc'
for theme in folders:
theme_path = os.path.join(emoticons_data_path, theme, file)
if os.path.exists(theme_path):
emoticons_themes.append(theme)
files += os.listdir(configpaths.get('MY_EMOTS'))
for file in files:
if file.endswith('.png'):
emoticons_themes.append(file[:-4])
emoticons_themes.sort()
return emoticons_themes
def get_emoticon_theme_path(theme):
emoticons_data_path = os.path.join(configpaths.get('DATA'), 'emoticons', theme)
if os.path.exists(emoticons_data_path):
return emoticons_data_path
emoticons_user_path = os.path.join(configpaths.get('MY_EMOTS'), theme)
if os.path.exists(emoticons_user_path):
return emoticons_user_path
def call_counter(func):
def helper(self, restart=False):
if restart:
......
......@@ -34,7 +34,7 @@
from gi.repository import GLib
import time
import os
from gajim import dialogs
import queue
import urllib
......@@ -43,12 +43,14 @@
from gajim.gtk.util import load_icon
from gajim.gtk.util import get_builder
from gajim.gtk.util import get_cursor
from gajim.gtk.emoji_data import emoji_pixbufs
from gajim.gtk.emoji_data import is_emoji
from gajim.gtk.emoji_data import get_emoji_pixbuf
from gajim.common import app
from gajim.common import helpers
from gajim.common import i18n
from calendar import timegm
from gajim.common.fuzzyclock import FuzzyClock
from gajim import emoticons
from gajim.common.const import StyleAttr
from gajim.htmltextview import HtmlTextView
......@@ -197,7 +199,6 @@ def __init__(self, account, used_in_history_window = False):
self.tv.set_left_margin(2)
self.tv.set_right_margin(2)
self.handlers = {}
self.images = []
self.image_cache = {}
self.xep0184_marks = {}
# self.last_sent_message_id = msg_stanza_id
......@@ -927,16 +928,32 @@ def print_special_text(self, special_text, other_tags, graphics=True,
else:
end_iter = buffer_.get_end_iter()
pixbuf = emoticons.get_pixbuf(special_text)
if app.config.get('emoticons_theme') and pixbuf and graphics:
theme = app.config.get('emoticons_theme')
show_emojis = theme and theme != 'font'
if show_emojis and graphics and is_emoji(special_text):
# it's an emoticon
anchor = buffer_.create_child_anchor(end_iter)
img = TextViewImage(anchor,
GLib.markup_escape_text(special_text))
img.set_from_pixbuf(pixbuf)
img.show()
self.images.append(img)
self.tv.add_child_at_anchor(img, anchor)
if emoji_pixbufs.complete:
# only search for the pixbuf if we are sure
# that loading is completed
pixbuf = get_emoji_pixbuf(special_text)
if pixbuf is None:
buffer_.insert(end_iter, special_text)
else:
pixbuf = pixbuf.copy()
anchor = buffer_.create_child_anchor(end_iter)
anchor.plaintext = special_text
img = Gtk.Image.new_from_pixbuf(pixbuf)
img.show()
self.tv.add_child_at_anchor(img, anchor)
else:
# Set marks and save them so we can replace the emojis
# once the loading is complete
start_mark = buffer_.create_mark(None, end_iter, True)
buffer_.insert(end_iter, special_text)
end_mark = buffer_.create_mark(None, end_iter, True)
emoji_pixbufs.append_marks(
self.tv, start_mark, end_mark, special_text)
elif special_text.startswith('www.') or \
special_text.startswith('ftp.') or \
text_is_valid_uri and not is_xhtml_link:
......
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchEntry" id="search">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<signal name="search-changed" handler="_search_changed" swapped="no"/>
<signal name="stop-search" handler="_search_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">350</property>
<property name="min_content_height">300</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="section_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">list</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">end</property>
<property name="pixel_size">72</property>
<property name="icon_name">edit-find-symbolic</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">start</property>
<property name="margin_top">12</property>
<property name="label" translatable="yes">No Results Found</property>
<style>
<class name="dim-label"/>
<class name="bold24"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">not-found</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>
......@@ -64,9 +64,13 @@ .VCard-GtkLinkButton { padding-left: 5px; border-left: none; }
popover#EmoticonPopover button { background: none; border: none; box-shadow:none; padding: 0px;}
popover#EmoticonPopover button > label { font-size: 24px; }
popover#EmoticonPopover flowboxchild > label { font-size: 24px; }
popover#EmoticonPopover notebook label { font-size: 24px; }
popover#EmoticonPopover flowbox { padding-left: 5px; padding-right: 6px; }
popover#EmoticonPopover flowboxchild { padding-top: 5px; padding-bottom: 5px; }
popover#EmoticonPopover scrolledwindow { border: none; }
popover#EmoticonPopover { padding: 5px; background-color: @theme_unfocused_base_color}
.emoji-chooser-heading { font-size: 13px; font-weight: bold; padding: 5px;}
.emoji-chooser-flowbox { padding-left: 5px; padding-right: 11px; }
.emoji-modifier-chooser-flowbox { padding-left: 5px; }
/* HistorySyncAssistant */
#HistorySyncAssistant list { border: 1px solid; border-color: @borders; }
......@@ -187,6 +191,7 @@ .theme_popover_listbox row:focus { outline: none; }
/* Text style */
.bold16 { font-size: 16px; font-weight: bold; }
.bold24 { font-size: 24px; font-weight: bold; }
.large-header { font-size: 20px; font-weight: bold; }
.status-away { color: #ff8533;}
.status-dnd { color: #e62e00;}
......
# -*- coding:utf-8 -*-
#
# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import logging
from collections import OrderedDict
from importlib.machinery import SourceFileLoader
from gi.repository import GdkPixbuf, Gtk, GLib
MODIFIER_MAX_CHILDREN_PER_LINE = 6
MAX_CHILDREN_PER_LINE = 10
MIN_HEIGHT = 200
pixbufs = dict()
codepoints = dict()
popover_instance = None
log = logging.getLogger('gajim.emoticons')
class SubPixbuf:
height = 24
width = 24
columns = 20
def __init__(self, path):
self.cur_column = 0
self.src_x = 0
self.src_y = 0
self.atlas = GdkPixbuf.Pixbuf.new_from_file(path)
def get_pixbuf(self):
self.src_x = self.cur_column * self.width
subpixbuf = self.atlas.new_subpixbuf(self.src_x, self.src_y, self.width, self.height)
if self.cur_column == self.columns - 1:
self.src_y += self.width
self.cur_column = 0
else:
self.cur_column += 1
return subpixbuf
def load(path, ascii_emoticons):
module_name = 'emoticons_theme.py'
theme_path = os.path.join(path, module_name)
if sys.platform == 'win32' and not os.path.exists(theme_path):
module_name = 'emoticons_theme.pyc'
theme_path = os.path.join(path, module_name)
loader = SourceFileLoader(module_name, theme_path)
try:
theme = loader.load_module()
except FileNotFoundError:
log.exception('Emoticons theme not found')
return
if not theme.use_image:
# Use Font to display emoticons
set_popover(theme.emoticons, False)
return True
try:
sub = SubPixbuf(os.path.join(path, 'emoticons.png'))
except GLib.GError:
log.exception('Error while creating subpixbuf')
return False
def add_emoticon(codepoint_, sub, mod_list=None):
pix = sub.get_pixbuf()
for alternate in codepoint_:
if not ascii_emoticons:
try:
alternate.encode('ascii')
continue
except UnicodeEncodeError:
pass
codepoints[alternate] = pix
if pix not in pixbufs:
pixbufs[pix] = alternate
if mod_list is not None:
mod_list.append(pix)
else:
pixbuf_list.append(pix)
popover_dict = OrderedDict()
try:
for category in theme.emoticons:
if not theme.emoticons[category]:
# Empty category
continue
pixbuf_list = []
for filename, codepoint_ in theme.emoticons[category]:
if codepoint_ is None:
# Category image
pixbuf_list.append(sub.get_pixbuf())
continue
if not filename:
# We have an emoticon with a modifier
mod_list = []
for _, mod_codepoint in codepoint_:
add_emoticon(mod_codepoint, sub, mod_list)
pixbuf_list.append(mod_list)
else:
add_emoticon(codepoint_, sub)
popover_dict[category] = pixbuf_list
except Exception:
log.exception('Error while loading emoticon theme')
return
set_popover(popover_dict, True)
return True
def set_popover(popover_dict, use_image):
global popover_instance
popover_instance = EmoticonPopover(popover_dict, use_image)
def get_popover():
return popover_instance
def get_pixbuf(codepoint_):
try:
return codepoints[codepoint_]
except KeyError:
return None
def get_codepoint(pixbuf_):
try:
return pixbufs[pixbuf_]
except KeyError:
return None
def replace_with_codepoint(buffer_):
if not pixbufs:
# We use font emoticons
return
iter_ = buffer_.get_start_iter()
pix = iter_.get_pixbuf()
def replace(pix):
if pix:
emote = get_codepoint(pix)
if not emote:
return
iter_2 = iter_.copy()
iter_2.forward_char()
buffer_.delete(iter_, iter_2)
buffer_.insert(iter_, emote)
replace(pix)
while iter_.forward_char():
pix = iter_.get_pixbuf()
replace(pix)
class EmoticonPopover(Gtk.Popover):
def __init__(self, emoji_dict, use_image):
super().__init__()
self.set_name('EmoticonPopover')
self.text_widget = None
self.use_image = use_image
notebook = Gtk.Notebook()
self.add(notebook)
self.handler_id = self.connect('key_press_event', self.on_key_press)
for category in emoji_dict:
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_min_content_height(MIN_HEIGHT)
flowbox = Gtk.FlowBox()
flowbox.set_max_children_per_line(MAX_CHILDREN_PER_LINE)
flowbox.connect('child_activated', self.on_emoticon_press)
scrolled_window.add(flowbox)
# Use first entry as a label for the notebook page
if self.use_image:
cat_image = Gtk.Image()
cat_image.set_from_pixbuf(emoji_dict[category][0])
notebook.append_page(scrolled_window, cat_image)
else:
notebook.append_page(scrolled_window, Gtk.Label(label=emoji_dict[category][0]))
# Populate the category with emojis
for pix in emoji_dict[category][1:]:
if isinstance(pix, list):
widget = self.add_emoticon_modifier(pix)
else:
if self.use_image:
widget = Gtk.Image()
widget.set_from_pixbuf(pix)
else:
widget = Gtk.Label(label=pix)
flowbox.add(widget)
notebook.show_all()
def add_emoticon_modifier(self, pixbuf_list):
button = Gtk.MenuButton()
button.set_relief(Gtk.ReliefStyle.NONE)
if self.use_image:
# We use the first item of the list as image for the button
button.get_child().set_from_pixbuf(pixbuf_list[0])
else:
button.remove(button.get_child())
label = Gtk.Label(label=pixbuf_list[0])
button.add(label)
button.connect('button-press-event', self.on_modifier_press)
popover = Gtk.Popover()
popover.set_name('EmoticonPopover')
popover.connect('key_press_event', self.on_key_press)
flowbox = Gtk.FlowBox()
flowbox.set_size_request(200, -1)
flowbox.set_max_children_per_line(MODIFIER_MAX_CHILDREN_PER_LINE)
flowbox.connect('child_activated', self.on_emoticon_press)
popover.add(flowbox)
for pix in pixbuf_list[1:]:
if self.use_image:
widget = Gtk.Image()
widget.set_from_pixbuf(pix)
else:
widget = Gtk.Label(label=pix)
flowbox.add(widget)
flowbox.show_all()
button.set_popover(popover)
return button
def set_callbacks(self, widget):
self.text_widget = widget
# Because the handlers getting disconnected when on_destroy() is called
# we connect them again
if self.handler_id:
self.disconnect(self.handler_id)
self.handler_id = self.connect('key_press_event', self.on_key_press)
def on_key_press(self, widget, event):
self.text_widget.grab_focus()
def on_modifier_press(self, button, event):
if event.button == 3:
button.get_popover().show()
button.get_popover().get_child().unselect_all()
if event.button == 1:
button.get_parent().emit('activate')
if self.use_image:
self.append_emoticon(button.get_child().get_pixbuf())
else:
self.append_emoticon(button.get_child().get_text())
return True
def on_emoticon_press(self, flowbox, child):
GLib.timeout_add(100, flowbox.unselect_all)