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

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.gtk.util import convert_rgb_to_hex
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 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
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 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
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 @@ class ConfigPaths:
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 GObject
from gi.repository import GLib
import time
import os
from gajim import dialogs
import queue
import urllib
......@@ -43,12 +43,14 @@ from gajim.gtk import util
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 @@ class ConversationTextview(GObject.GObject):
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 @@ class ConversationTextview(GObject.GObject):
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.
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 @@
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 @@ list.settings > row > box {
/* 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;}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -557,8 +557,8 @@ class Preferences(Gtk.ApplicationWindow):
else:
app.config.set('emoticons_theme', emot_theme)
app.interface.init_emoticons()
app.interface.make_regexps()
from gajim.gtk.emoji_chooser import emoji_chooser
emoji_chooser.load()
self.toggle_emoticons()
def toggle_emoticons(self):
......
......@@ -101,7 +101,6 @@ from gajim.common.connection_handlers_events import (
from gajim.common.modules.httpupload import HTTPUploadProgressEvent
from gajim.common.connection import Connection
from gajim.common.file_props import FilesProp
from gajim import emoticons
from gajim.common.const import AvatarSize, SSLError, PEPEventType
from gajim.common.const import ACTIVITIES, MOODS
......@@ -111,6 +110,7 @@ from threading import Thread
from gajim.common import ged
from gajim.common.caps_cache import muc_caps_cache
from gajim.gtk.emoji_data import emoji_data, emoji_ascii_data
from gajim.gtk import JoinGroupchatWindow
from gajim.gtk import ErrorDialog
from gajim.gtk import WarningDialog
......@@ -1794,8 +1794,8 @@ class Interface:
@property
def emot_and_basic_re(self):
if not self._emot_and_basic_re:
self._emot_and_basic_re = re.compile(self.emot_and_basic,
re.IGNORECASE + re.UNICODE)
self._emot_and_basic_re = re.compile(
self.emot_and_basic, re.IGNORECASE)
return self._emot_and_basic_re
@property
......@@ -1867,43 +1867,13 @@ class Interface:
basic_pattern += formatting
self.basic_pattern = basic_pattern
emoticons_pattern = ''
if app.config.get('emoticons_theme'):
# When an emoticon is bordered by an alpha-numeric character it is
# NOT expanded. e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc
# We still allow multiple emoticons side-by-side like :P:P:P
# sort keys by length so :qwe emot is checked before :q
keys = sorted(emoticons.codepoints.keys(), key=len, reverse=True)
emoticons_pattern_prematch = ''
emoticons_pattern_postmatch = ''
emoticon_length = 0
for emoticon in keys: # travel thru emoticons list
emoticon_escaped = re.escape(emoticon) # escape regexp metachars
# | means or in regexp
emoticons_pattern += emoticon_escaped + '|'
if (emoticon_length != len(emoticon)):
# Build up expressions to match emoticons next to others
emoticons_pattern_prematch = \
emoticons_pattern_prematch[:-1] + ')|(?<='
emoticons_pattern_postmatch = \
emoticons_pattern_postmatch[:-1] + ')|(?='
emoticon_length = len(emoticon)
emoticons_pattern_prematch += emoticon_escaped + '|'
emoticons_pattern_postmatch += emoticon_escaped + '|'
# We match from our list of emoticons, but they must either have
# whitespace, or another emoticon next to it to match successfully
# [\w.] alphanumeric and dot (for not matching 8) in (2.8))
emoticons_pattern = '|' + r'(?:(?<![\w.]' + \
emoticons_pattern_prematch[:-1] + '))' + '(?:' + \
emoticons_pattern[:-1] + ')' + r'(?:(?![\w]' + \
emoticons_pattern_postmatch[:-1] + '))'
# because emoticons match later (in the string) they need to be after
# basic matches that may occur earlier
self.emot_and_basic = basic_pattern + emoticons_pattern
# needed for xhtml display
self.emot_only = emoticons_pattern
emoticons = emoji_data.get_regex()
if app.config.get('ascii_emoticons'):
emoticons += '|%s' % emoji_ascii_data.get_regex()
pass
self.emot_and_basic = '%s|%s' % (basic_pattern, emoticons)
# at least one character in 3 parts (before @, after @, after .)
self.sth_at_sth_dot_sth = r'\S+@\S+\.\S*[^\s)?]'
......@@ -1912,30 +1882,6 @@ class Interface:
self.invalid_XML_chars = '[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x1f]|'\
'[\ud800-\udfff]|[\ufffe-\uffff]'
def init_emoticons(self):
emot_theme = app.config.get('emoticons_theme')
ascii_emoticons = app.config.get('ascii_emoticons')
if not emot_theme:
return
themes = helpers.get_available_emoticon_themes()
if emot_theme not in themes:
if 'font-emoticons' in themes:
emot_theme = 'font-emoticons'
app.config.set('emoticons_theme', 'font-emoticons')
else:
app.config.set('emoticons_theme', '')
return
path = helpers.get_emoticon_theme_path(emot_theme)
if not emoticons.load(path, ascii_emoticons):
WarningDialog(
_('Emoticons disabled'),
_('Your configured emoticons theme could not be loaded.'
' See the log for more details.'),
transient_for=app.get_app_window('Preferences'))
app.config.set('emoticons_theme', '')
return
################################################################################
### Methods for opening new messages controls
......@@ -2667,7 +2613,6 @@ class Interface:
self.basic_pattern = None
self.emot_and_basic = None
self.sth_at_sth_dot_sth = None
self.emot_only = None
cfg_was_read = parser.read()
......@@ -2814,7 +2759,9 @@ class Interface:
# set the icon to all windows
Gtk.Window.set_default_icon_list(pixs)
self.init_emoticons()
# Init emoji_chooser
from gajim.gtk.emoji_chooser import emoji_chooser
emoji_chooser.load()
self.make_regexps()
# get transports type from DB
......
......@@ -1081,6 +1081,20 @@ class HtmlTextView(Gtk.TextView):
search_iter.forward_char()
return selection
def replace_emojis(self, start_mark, end_mark, pixbuf, codepoint):
buffer_ = self.get_buffer()
start_iter = buffer_.get_iter_at_mark(start_mark)
end_iter = buffer_.get_iter_at_mark(end_mark)
buffer_.delete(start_iter, end_iter)
anchor = buffer_.create_child_anchor(start_iter)
anchor.plaintext = codepoint
emoji = Gtk.Image.new_from_pixbuf(pixbuf)
emoji.show()
self.add_child_at_anchor(emoji, anchor)
buffer_.delete_mark(start_mark)
buffer_.delete_mark(end_mark)
change_cursor = None
if __name__ == '__main__':
......
......@@ -350,6 +350,53 @@ class MessageTextView(Gtk.TextView):
else:
return None
def replace_emojis(self):
theme = app.config.get('emoticons_theme')
if not theme or theme == 'font':
return
def replace(anchor):
if anchor is None:
return
image = anchor.get_widgets()[0]
if hasattr(image, 'codepoint'):
# found emoji
self.replace_char_at_iter(iter_, image.codepoint)
image.destroy()
iter_ = self.get_buffer().get_start_iter()
replace(iter_.get_child_anchor())
while iter_.forward_char():
replace(iter_.get_child_anchor())
def replace_char_at_iter(self, iter_, new_char):
buffer_ = self.get_buffer()
iter_2 = iter_.copy()
iter_2.forward_char()
buffer_.delete(iter_, iter_2)
buffer_.insert(iter_, new_char)
def insert_emoji(self, codepoint, pixbuf):
self.remove_placeholder()
buffer_ = self.get_buffer()
if buffer_.get_char_count():
# buffer contains text
buffer_.insert_at_cursor(' ')
insert_mark = buffer_.get_insert()
insert_iter = buffer_.get_iter_at_mark(insert_mark)
if pixbuf is None:
buffer_.insert(insert_iter, codepoint)
else:
anchor = buffer_.create_child_anchor(insert_iter)
image = Gtk.Image.new_from_pixbuf(pixbuf)
image.codepoint = codepoint
image.show()
self.add_child_at_anchor(image, anchor)
buffer_.insert_at_cursor(' ')
def destroy(self):
GLib.idle_add(gc.collect)
......
......@@ -214,8 +214,7 @@ class update_po(Command):
package_data_activities = ['data/activities/*/*/*.png']
package_data_emoticons = ['data/emoticons/*/emoticons_theme.py',
'data/emoticons/*/*.png',
package_data_emoticons = ['data/emoticons/*/*.png',
'data/emoticons/*/LICENSE']
package_data_gui = ['data/gui/*.ui']
package_data_icons = ['data/icons/hicolor/*/*/*.png',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment