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
This commit is contained in:
parent
e37ab6b59a
commit
5feb4becfd
|
@ -42,7 +42,7 @@ from gajim.gtk.util import convert_rgb_to_hex
|
||||||
from gajim import notify
|
from gajim import notify
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from gajim import emoticons
|
from gajim.gtk.emoji_chooser import emoji_chooser
|
||||||
from gajim.common import events
|
from gajim.common import events
|
||||||
from gajim.common import app
|
from gajim.common import app
|
||||||
from gajim.common import helpers
|
from gajim.common import helpers
|
||||||
|
@ -676,7 +676,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
||||||
event.keyval == Gdk.KEY_KP_Enter: # ENTER
|
event.keyval == Gdk.KEY_KP_Enter: # ENTER
|
||||||
message_textview = widget
|
message_textview = widget
|
||||||
message_buffer = message_textview.get_buffer()
|
message_buffer = message_textview.get_buffer()
|
||||||
emoticons.replace_with_codepoint(message_buffer)
|
message_textview.replace_emojis()
|
||||||
start_iter, end_iter = message_buffer.get_bounds()
|
start_iter, end_iter = message_buffer.get_bounds()
|
||||||
message = message_buffer.get_text(start_iter, end_iter, False)
|
message = message_buffer.get_text(start_iter, end_iter, False)
|
||||||
xhtml = self.msg_textview.get_xhtml()
|
xhtml = self.msg_textview.get_xhtml()
|
||||||
|
@ -1055,10 +1055,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
||||||
if not self.parent_win:
|
if not self.parent_win:
|
||||||
return
|
return
|
||||||
|
|
||||||
popover = emoticons.get_popover()
|
emoji_chooser.text_widget = self.msg_textview
|
||||||
popover.set_callbacks(self.msg_textview)
|
|
||||||
emoticons_button = self.xml.get_object('emoticons_button')
|
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):
|
def on_color_menuitem_activate(self, widget):
|
||||||
color_dialog = Gtk.ColorChooserDialog(None, self.parent_win.window)
|
color_dialog = Gtk.ColorChooserDialog(None, self.parent_win.window)
|
||||||
|
|
|
@ -129,6 +129,7 @@ class ConfigPaths:
|
||||||
source_paths = [
|
source_paths = [
|
||||||
('DATA', os.path.join(basedir, 'data')),
|
('DATA', os.path.join(basedir, 'data')),
|
||||||
('STYLE', os.path.join(basedir, 'data', 'style')),
|
('STYLE', os.path.join(basedir, 'data', 'style')),
|
||||||
|
('EMOTICONS', os.path.join(basedir, 'data', 'emoticons')),
|
||||||
('GUI', os.path.join(basedir, 'data', 'gui')),
|
('GUI', os.path.join(basedir, 'data', 'gui')),
|
||||||
('ICONS', os.path.join(basedir, 'data', 'icons')),
|
('ICONS', os.path.join(basedir, 'data', 'icons')),
|
||||||
('HOME', os.path.expanduser('~')),
|
('HOME', os.path.expanduser('~')),
|
||||||
|
|
|
@ -1479,36 +1479,29 @@ def version_condition(current_version, required_version):
|
||||||
|
|
||||||
def get_available_emoticon_themes():
|
def get_available_emoticon_themes():
|
||||||
emoticons_themes = []
|
emoticons_themes = []
|
||||||
emoticons_data_path = os.path.join(configpaths.get('DATA'), 'emoticons')
|
if sys.platform not in ('win32', 'darwin'):
|
||||||
font_theme_path = os.path.join(
|
# Colored emoji fonts only supported on Linux
|
||||||
configpaths.get('DATA'), 'emoticons', 'font-emoticons', 'emoticons_theme.py')
|
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')):
|
if os.path.isdir(configpaths.get('MY_EMOTS')):
|
||||||
folders += os.listdir(configpaths.get('MY_EMOTS'))
|
files += os.listdir(configpaths.get('MY_EMOTS'))
|
||||||
|
|
||||||
file = 'emoticons_theme.py'
|
for file in files:
|
||||||
if os.name == 'nt' and not os.path.exists(font_theme_path):
|
if file.endswith('.png'):
|
||||||
# When starting Gajim from source .py files are available
|
emoticons_themes.append(file[:-4])
|
||||||
# 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)
|
|
||||||
emoticons_themes.sort()
|
emoticons_themes.sort()
|
||||||
return emoticons_themes
|
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 call_counter(func):
|
||||||
def helper(self, restart=False):
|
def helper(self, restart=False):
|
||||||
if restart:
|
if restart:
|
||||||
|
|
|
@ -34,7 +34,7 @@ from gi.repository import GObject
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
from gajim import dialogs
|
|
||||||
import queue
|
import queue
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
|
@ -43,12 +43,14 @@ from gajim.gtk import util
|
||||||
from gajim.gtk.util import load_icon
|
from gajim.gtk.util import load_icon
|
||||||
from gajim.gtk.util import get_builder
|
from gajim.gtk.util import get_builder
|
||||||
from gajim.gtk.util import get_cursor
|
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 app
|
||||||
from gajim.common import helpers
|
from gajim.common import helpers
|
||||||
from gajim.common import i18n
|
from gajim.common import i18n
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
from gajim.common.fuzzyclock import FuzzyClock
|
from gajim.common.fuzzyclock import FuzzyClock
|
||||||
from gajim import emoticons
|
|
||||||
from gajim.common.const import StyleAttr
|
from gajim.common.const import StyleAttr
|
||||||
|
|
||||||
from gajim.htmltextview import HtmlTextView
|
from gajim.htmltextview import HtmlTextView
|
||||||
|
@ -197,7 +199,6 @@ class ConversationTextview(GObject.GObject):
|
||||||
self.tv.set_left_margin(2)
|
self.tv.set_left_margin(2)
|
||||||
self.tv.set_right_margin(2)
|
self.tv.set_right_margin(2)
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
self.images = []
|
|
||||||
self.image_cache = {}
|
self.image_cache = {}
|
||||||
self.xep0184_marks = {}
|
self.xep0184_marks = {}
|
||||||
# self.last_sent_message_id = msg_stanza_id
|
# self.last_sent_message_id = msg_stanza_id
|
||||||
|
@ -927,16 +928,32 @@ class ConversationTextview(GObject.GObject):
|
||||||
else:
|
else:
|
||||||
end_iter = buffer_.get_end_iter()
|
end_iter = buffer_.get_end_iter()
|
||||||
|
|
||||||
pixbuf = emoticons.get_pixbuf(special_text)
|
theme = app.config.get('emoticons_theme')
|
||||||
if app.config.get('emoticons_theme') and pixbuf and graphics:
|
show_emojis = theme and theme != 'font'
|
||||||
|
if show_emojis and graphics and is_emoji(special_text):
|
||||||
# it's an emoticon
|
# it's an emoticon
|
||||||
|
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 = buffer_.create_child_anchor(end_iter)
|
||||||
img = TextViewImage(anchor,
|
anchor.plaintext = special_text
|
||||||
GLib.markup_escape_text(special_text))
|
img = Gtk.Image.new_from_pixbuf(pixbuf)
|
||||||
img.set_from_pixbuf(pixbuf)
|
|
||||||
img.show()
|
img.show()
|
||||||
self.images.append(img)
|
|
||||||
self.tv.add_child_at_anchor(img, anchor)
|
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 \
|
elif special_text.startswith('www.') or \
|
||||||
special_text.startswith('ftp.') or \
|
special_text.startswith('ftp.') or \
|
||||||
text_is_valid_uri and not is_xhtml_link:
|
text_is_valid_uri and not is_xhtml_link:
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
|
@ -0,0 +1,124 @@
|
||||||
|
<?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 { background: none; border: none; box-shadow:none; padding: 0px;}
|
||||||
popover#EmoticonPopover button > label { font-size: 24px; }
|
popover#EmoticonPopover button > label { font-size: 24px; }
|
||||||
popover#EmoticonPopover flowboxchild > 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 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 */
|
||||||
#HistorySyncAssistant list { border: 1px solid; border-color: @borders; }
|
#HistorySyncAssistant list { border: 1px solid; border-color: @borders; }
|
||||||
|
@ -187,6 +191,7 @@ list.settings > row > box {
|
||||||
/* Text style */
|
/* Text style */
|
||||||
|
|
||||||
.bold16 { font-size: 16px; font-weight: bold; }
|
.bold16 { font-size: 16px; font-weight: bold; }
|
||||||
|
.bold24 { font-size: 24px; font-weight: bold; }
|
||||||
.large-header { font-size: 20px; font-weight: bold; }
|
.large-header { font-size: 20px; font-weight: bold; }
|
||||||
.status-away { color: #ff8533;}
|
.status-away { color: #ff8533;}
|
||||||
.status-dnd { color: #e62e00;}
|
.status-dnd { color: #e62e00;}
|
||||||
|
|
|
@ -1,321 +0,0 @@
|
||||||
# -*- 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)
|
|
||||||
|
|
||||||
if isinstance(child.get_child(), Gtk.MenuButton):
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.use_image:
|
|
||||||
self.append_emoticon(child.get_child().get_pixbuf())
|
|
||||||
else:
|
|
||||||
self.append_emoticon(child.get_child().get_text())
|
|
||||||
|
|
||||||
def append_emoticon(self, pix):
|
|
||||||
self.text_widget.remove_placeholder()
|
|
||||||
buffer_ = self.text_widget.get_buffer()
|
|
||||||
if buffer_.get_char_count():
|
|
||||||
buffer_.insert_at_cursor(' ')
|
|
||||||
insert_mark = buffer_.get_insert()
|
|
||||||
insert_iter = buffer_.get_iter_at_mark(insert_mark)
|
|
||||||
if self.use_image:
|
|
||||||
buffer_.insert_pixbuf(insert_iter, pix)
|
|
||||||
else:
|
|
||||||
buffer_.insert(insert_iter, pix)
|
|
||||||
buffer_.insert_at_cursor(' ')
|
|
||||||
else: # we are the beginning of buffer
|
|
||||||
insert_mark = buffer_.get_insert()
|
|
||||||
insert_iter = buffer_.get_iter_at_mark(insert_mark)
|
|
||||||
if self.use_image:
|
|
||||||
buffer_.insert_pixbuf(insert_iter, pix)
|
|
||||||
else:
|
|
||||||
buffer_.insert(insert_iter, pix)
|
|
||||||
buffer_.insert_at_cursor(' ')
|
|
||||||
|
|
||||||
def do_destroy(self):
|
|
||||||
# Remove the references we hold to other objects
|
|
||||||
self.text_widget = None
|
|
||||||
# Even though we don't destroy the Popover, handlers are getting
|
|
||||||
# still disconnected, which makes the handler_id invalid
|
|
||||||
# FIXME: find out how we can prevent handlers getting disconnected
|
|
||||||
self.handler_id = None
|
|
||||||
# Never destroy, creating a new EmoticonPopover is expensive
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,382 @@
|
||||||
|
# This file is part of Gajim.
|
||||||
|
#
|
||||||
|
# Gajim 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; version 3 only.
|
||||||
|
#
|
||||||
|
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import weakref
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import GLib
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
from gajim.common import app
|
||||||
|
from gajim.common import helpers
|
||||||
|
from gajim.common import configpaths
|
||||||
|
|
||||||
|
from gajim.gtk.util import get_builder
|
||||||
|
from gajim.gtk.emoji_data import emoji_data
|
||||||
|
from gajim.gtk.emoji_data import emoji_pixbufs
|
||||||
|
from gajim.gtk.emoji_data import Emoji
|
||||||
|
|
||||||
|
MODIFIER_MAX_CHILDREN_PER_LINE = 6
|
||||||
|
MAX_CHILDREN_PER_LINE = 10
|
||||||
|
|
||||||
|
log = logging.getLogger('gajim.emoji')
|
||||||
|
|
||||||
|
|
||||||
|
class Section(Gtk.Box):
|
||||||
|
def __init__(self, name, search_entry, press_cb):
|
||||||
|
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
self._press_cb = press_cb
|
||||||
|
self.pixbuf_generator = None
|
||||||
|
self.heading = Gtk.Label(label=name)
|
||||||
|
self.heading.set_halign(Gtk.Align.START)
|
||||||
|
self.heading.get_style_context().add_class('emoji-chooser-heading')
|
||||||
|
self.add(self.heading)
|
||||||
|
|
||||||
|
self.flowbox = Gtk.FlowBox()
|
||||||
|
self.flowbox.get_style_context().add_class('emoji-chooser-flowbox')
|
||||||
|
self.flowbox.set_max_children_per_line(MAX_CHILDREN_PER_LINE)
|
||||||
|
self.flowbox.set_filter_func(self._filter_func, search_entry)
|
||||||
|
self.flowbox.connect('child-activated', press_cb)
|
||||||
|
|
||||||
|
self.add(self.flowbox)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def _filter_func(self, child, search_entry):
|
||||||
|
name = search_entry.get_text()
|
||||||
|
if not name:
|
||||||
|
self.show()
|
||||||
|
return True
|
||||||
|
|
||||||
|
if name in child.desc:
|
||||||
|
self.show()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_emoji(self, codepoint, attrs):
|
||||||
|
# Return always True, this method is used for the emoji factory
|
||||||
|
# called by GLib.idle_add()
|
||||||
|
pixbuf = self._get_next_pixbuf()
|
||||||
|
|
||||||
|
variations = attrs.get('variations', None)
|
||||||
|
if variations is None:
|
||||||
|
if pixbuf is None:
|
||||||
|
return True
|
||||||
|
self.flowbox.add(EmojiChild(codepoint, pixbuf, attrs['desc']))
|
||||||
|
if pixbuf != 'font':
|
||||||
|
# We save the pixbuf for fast access if we need to
|
||||||
|
# replace a codepoint in the textview
|
||||||
|
emoji_pixbufs[codepoint] = pixbuf
|
||||||
|
else:
|
||||||
|
if pixbuf is not None:
|
||||||
|
chooser = self._get_emoji_modifier(
|
||||||
|
codepoint, pixbuf, attrs)
|
||||||
|
chooser.flowbox.connect(
|
||||||
|
'child-activated', self._press_cb)
|
||||||
|
|
||||||
|
if pixbuf != 'font':
|
||||||
|
emoji_pixbufs[codepoint] = pixbuf
|
||||||
|
|
||||||
|
for codepoint, attrs in variations.items():
|
||||||
|
pixbuf = self._get_next_pixbuf()
|
||||||
|
if pixbuf is None:
|
||||||
|
continue
|
||||||
|
chooser.add_emoji(codepoint, pixbuf)
|
||||||
|
|
||||||
|
if pixbuf != 'font':
|
||||||
|
emoji_pixbufs[codepoint] = pixbuf
|
||||||
|
else:
|
||||||
|
# We dont have a image for the base codepoint
|
||||||
|
# so skip all modifiers of it
|
||||||
|
for codepoint, attrs in variations.items():
|
||||||
|
pixbuf = self._get_next_pixbuf()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear_emojis(self):
|
||||||
|
def _remove_emoji(emoji):
|
||||||
|
self.flowbox.remove(emoji)
|
||||||
|
emoji.destroy()
|
||||||
|
self.flowbox.foreach(_remove_emoji)
|
||||||
|
|
||||||
|
def _get_emoji_modifier(self, codepoint, pixbuf, attrs):
|
||||||
|
chooser = ModifierChooser()
|
||||||
|
modifier_button = EmojiModifierChild(codepoint, pixbuf, attrs['desc'])
|
||||||
|
modifier_button.button.set_popover(chooser)
|
||||||
|
self.flowbox.add(modifier_button)
|
||||||
|
return chooser
|
||||||
|
|
||||||
|
def _get_next_pixbuf(self):
|
||||||
|
if self.pixbuf_generator is None:
|
||||||
|
return 'font'
|
||||||
|
return next(self.pixbuf_generator, False)
|
||||||
|
|
||||||
|
|
||||||
|
class EmojiChild(Gtk.FlowBoxChild):
|
||||||
|
def __init__(self, codepoint, pixbuf, desc):
|
||||||
|
Gtk.FlowBoxChild.__init__(self)
|
||||||
|
self.desc = desc
|
||||||
|
self.codepoint = codepoint
|
||||||
|
self.pixbuf = pixbuf
|
||||||
|
if pixbuf == 'font':
|
||||||
|
self.add(Gtk.Label(label=codepoint))
|
||||||
|
else:
|
||||||
|
self.add(Gtk.Image.new_from_pixbuf(pixbuf))
|
||||||
|
self.set_tooltip_text(desc)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def get_emoji(self):
|
||||||
|
if self.pixbuf != 'font':
|
||||||
|
pixbuf = self.get_child().get_pixbuf()
|
||||||
|
pixbuf = pixbuf.scale_simple(Emoji.INPUT_SIZE,
|
||||||
|
Emoji.INPUT_SIZE,
|
||||||
|
GdkPixbuf.InterpType.HYPER)
|
||||||
|
return self.codepoint, pixbuf
|
||||||
|
return self.codepoint, None
|
||||||
|
|
||||||
|
|
||||||
|
class EmojiModifierChild(Gtk.FlowBoxChild):
|
||||||
|
def __init__(self, codepoint, pixbuf, desc):
|
||||||
|
Gtk.FlowBoxChild.__init__(self)
|
||||||
|
self.desc = desc
|
||||||
|
self.codepoint = codepoint
|
||||||
|
self.pixbuf = pixbuf
|
||||||
|
|
||||||
|
self.button = Gtk.MenuButton()
|
||||||
|
self.button.set_relief(Gtk.ReliefStyle.NONE)
|
||||||
|
self.button.connect('button-press-event', self._button_press)
|
||||||
|
|
||||||
|
if pixbuf == 'font':
|
||||||
|
self.button.remove(self.button.get_child())
|
||||||
|
label = Gtk.Label(label=codepoint)
|
||||||
|
self.button.add(label)
|
||||||
|
else:
|
||||||
|
self.button.get_child().set_from_pixbuf(pixbuf)
|
||||||
|
|
||||||
|
self.set_tooltip_text(desc)
|
||||||
|
self.add(self.button)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def _button_press(self, button, event):
|
||||||
|
if event.button == 3:
|
||||||
|
button.get_popover().show()
|
||||||
|
button.get_popover().get_child().unselect_all()
|
||||||
|
return True
|
||||||
|
if event.button == 1:
|
||||||
|
self.get_parent().emit('child-activated', self)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_emoji(self):
|
||||||
|
if self.pixbuf != 'font':
|
||||||
|
pixbuf = self.button.get_child().get_pixbuf()
|
||||||
|
pixbuf = pixbuf.scale_simple(Emoji.INPUT_SIZE,
|
||||||
|
Emoji.INPUT_SIZE,
|
||||||
|
GdkPixbuf.InterpType.HYPER)
|
||||||
|
return self.codepoint, pixbuf
|
||||||
|
return self.codepoint, None
|
||||||
|
|
||||||
|
|
||||||
|
class ModifierChooser(Gtk.Popover):
|
||||||
|
def __init__(self):
|
||||||
|
Gtk.Popover.__init__(self)
|
||||||
|
self.set_name('EmoticonPopover')
|
||||||
|
|
||||||
|
self.flowbox = Gtk.FlowBox()
|
||||||
|
self.flowbox.get_style_context().add_class(
|
||||||
|
'emoji-modifier-chooser-flowbox')
|
||||||
|
self.flowbox.set_size_request(200, -1)
|
||||||
|
self.flowbox.set_max_children_per_line(MODIFIER_MAX_CHILDREN_PER_LINE)
|
||||||
|
self.flowbox.show()
|
||||||
|
self.add(self.flowbox)
|
||||||
|
|
||||||
|
def add_emoji(self, codepoint, pixbuf):
|
||||||
|
self.flowbox.add(EmojiChild(codepoint, pixbuf, None))
|
||||||
|
|
||||||
|
|
||||||
|
class EmojiChooser(Gtk.Popover):
|
||||||
|
|
||||||
|
_section_names = [
|
||||||
|
'Smileys & People',
|
||||||
|
'Animals & Nature',
|
||||||
|
'Food & Drink',
|
||||||
|
'Travel & Places',
|
||||||
|
'Activities',
|
||||||
|
'Objects',
|
||||||
|
'Symbols',
|
||||||
|
'Flags'
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.set_name('EmoticonPopover')
|
||||||
|
self._text_widget = None
|
||||||
|
self._load_source_id = None
|
||||||
|
|
||||||
|
self._builder = get_builder('emoji_chooser.ui')
|
||||||
|
self._search = self._builder.get_object('search')
|
||||||
|
self._stack = self._builder.get_object('stack')
|
||||||
|
|
||||||
|
self._sections = OrderedDict()
|
||||||
|
for name in self._section_names:
|
||||||
|
self._sections[name] = Section(
|
||||||
|
name, self._search, self._on_emoticon_press)
|
||||||
|
|
||||||
|
section_box = self._builder.get_object('section_box')
|
||||||
|
for section in self._sections.values():
|
||||||
|
section_box.add(section)
|
||||||
|
|
||||||
|
self.add(self._builder.get_object('box'))
|
||||||
|
|
||||||
|
self.connect('key-press-event', self._key_press)
|
||||||
|
self._builder.connect_signals(self)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text_widget(self):
|
||||||
|
return self._text_widget
|
||||||
|
|
||||||
|
@text_widget.setter
|
||||||
|
def text_widget(self, value):
|
||||||
|
# Hold only a weak reference so if we can destroy
|
||||||
|
# the ChatControl
|
||||||
|
self._text_widget = weakref.ref(value)
|
||||||
|
|
||||||
|
def _key_press(self, widget, event):
|
||||||
|
return self._search.handle_event(event)
|
||||||
|
|
||||||
|
def _search_changed(self, entry):
|
||||||
|
for section in self._sections.values():
|
||||||
|
section.hide()
|
||||||
|
section.flowbox.invalidate_filter()
|
||||||
|
self._switch_stack()
|
||||||
|
|
||||||
|
def _clear_sections(self):
|
||||||
|
for section in self._sections.values():
|
||||||
|
section.clear_emojis()
|
||||||
|
|
||||||
|
def _switch_stack(self):
|
||||||
|
for section in self._sections.values():
|
||||||
|
if section.is_visible():
|
||||||
|
self._stack.set_visible_child_name('list')
|
||||||
|
return
|
||||||
|
self._stack.set_visible_child_name('not-found')
|
||||||
|
|
||||||
|
def _get_current_theme(self):
|
||||||
|
theme = app.config.get('emoticons_theme')
|
||||||
|
if not theme:
|
||||||
|
log.warning('No emoji theme set')
|
||||||
|
return
|
||||||
|
|
||||||
|
themes = helpers.get_available_emoticon_themes()
|
||||||
|
if theme not in themes:
|
||||||
|
if sys.platform not in ('win32', 'darwin'):
|
||||||
|
app.config.set('emoticons_theme', 'font')
|
||||||
|
theme = 'font'
|
||||||
|
else:
|
||||||
|
# Win/Mac fallback to noto
|
||||||
|
app.config.set('emoticons_theme', 'noto')
|
||||||
|
theme = 'noto'
|
||||||
|
return theme
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_emoji_theme_path(theme):
|
||||||
|
if theme == 'font':
|
||||||
|
return 'font'
|
||||||
|
emoticons_data_path = os.path.join(configpaths.get('EMOTICONS'),
|
||||||
|
theme,
|
||||||
|
'%s.png' % theme)
|
||||||
|
if os.path.exists(emoticons_data_path):
|
||||||
|
return emoticons_data_path
|
||||||
|
|
||||||
|
emoticons_user_path = os.path.join(configpaths.get('MY_EMOTS'),
|
||||||
|
'%s.png' % theme)
|
||||||
|
if os.path.exists(emoticons_user_path):
|
||||||
|
return emoticons_user_path
|
||||||
|
|
||||||
|
log.warning('Could not find emoji theme: %s', theme)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
theme = self._get_current_theme()
|
||||||
|
path = self._get_emoji_theme_path(theme)
|
||||||
|
if not theme or path is None:
|
||||||
|
self._clear_sections()
|
||||||
|
emoji_pixbufs.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Attach pixbuf generator
|
||||||
|
pixbuf_generator = None
|
||||||
|
if theme != 'font':
|
||||||
|
pixbuf_generator = self._get_next_pixbuf(path)
|
||||||
|
for section in self._sections.values():
|
||||||
|
section.pixbuf_generator = pixbuf_generator
|
||||||
|
|
||||||
|
if self._load_source_id is not None:
|
||||||
|
GLib.source_remove(self._load_source_id)
|
||||||
|
|
||||||
|
# When we change emoji theme
|
||||||
|
self._clear_sections()
|
||||||
|
emoji_pixbufs.clear()
|
||||||
|
|
||||||
|
factory = self._emoji_factory()
|
||||||
|
self._load_source_id = GLib.idle_add(lambda: next(factory, False),
|
||||||
|
priority=GLib.PRIORITY_LOW)
|
||||||
|
|
||||||
|
def _emoji_factory(self):
|
||||||
|
for codepoint, attrs in emoji_data.items():
|
||||||
|
if not attrs['fully-qualified']:
|
||||||
|
# We dont add these to the UI
|
||||||
|
continue
|
||||||
|
|
||||||
|
section = self._sections[attrs['group']]
|
||||||
|
yield section.add_emoji(codepoint, attrs)
|
||||||
|
self._load_source_id = None
|
||||||
|
emoji_pixbufs.complete = True
|
||||||
|
|
||||||
|
def _get_next_pixbuf(self, path):
|
||||||
|
src_x = src_y = cur_column = 0
|
||||||
|
atlas = GdkPixbuf.Pixbuf.new_from_file(path)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
src_x = cur_column * Emoji.PARSE_WIDTH
|
||||||
|
subpixbuf = atlas.new_subpixbuf(src_x, src_y,
|
||||||
|
Emoji.PARSE_WIDTH,
|
||||||
|
Emoji.PARSE_HEIGHT)
|
||||||
|
|
||||||
|
if list(subpixbuf.get_pixels())[0:4] == [0, 0, 0, 255]:
|
||||||
|
# top left corner is a black pixel means image is missing
|
||||||
|
subpixbuf = None
|
||||||
|
|
||||||
|
if cur_column == Emoji.PARSE_COLUMNS - 1:
|
||||||
|
src_y += Emoji.PARSE_WIDTH
|
||||||
|
cur_column = 0
|
||||||
|
else:
|
||||||
|
cur_column += 1
|
||||||
|
|
||||||
|
yield subpixbuf
|
||||||
|
|
||||||
|
def _on_emoticon_press(self, flowbox, child):
|
||||||
|
GLib.timeout_add(100, flowbox.unselect_child, child)
|
||||||
|
codepoint, pixbuf = child.get_emoji()
|
||||||
|
self._text_widget().insert_emoji(codepoint, pixbuf)
|
||||||
|
|
||||||
|
def do_destroy(self):
|
||||||
|
# Remove the references we hold to other objects
|
||||||
|
self._text_widget = None
|
||||||
|
# Never destroy, creating a new EmoticonPopover is expensive
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
emoji_chooser = EmojiChooser()
|
File diff suppressed because it is too large
Load Diff
|
@ -557,8 +557,8 @@ class Preferences(Gtk.ApplicationWindow):
|
||||||
else:
|
else:
|
||||||
app.config.set('emoticons_theme', emot_theme)
|
app.config.set('emoticons_theme', emot_theme)
|
||||||
|
|
||||||
app.interface.init_emoticons()
|
from gajim.gtk.emoji_chooser import emoji_chooser
|
||||||
app.interface.make_regexps()
|
emoji_chooser.load()
|
||||||
self.toggle_emoticons()
|
self.toggle_emoticons()
|
||||||
|
|
||||||
def toggle_emoticons(self):
|
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.modules.httpupload import HTTPUploadProgressEvent
|
||||||
from gajim.common.connection import Connection
|
from gajim.common.connection import Connection
|
||||||
from gajim.common.file_props import FilesProp
|
from gajim.common.file_props import FilesProp
|
||||||
from gajim import emoticons
|
|
||||||
from gajim.common.const import AvatarSize, SSLError, PEPEventType
|
from gajim.common.const import AvatarSize, SSLError, PEPEventType
|
||||||
from gajim.common.const import ACTIVITIES, MOODS
|
from gajim.common.const import ACTIVITIES, MOODS
|
||||||
|
|
||||||
|
@ -111,6 +110,7 @@ from threading import Thread
|
||||||
from gajim.common import ged
|
from gajim.common import ged
|
||||||
from gajim.common.caps_cache import muc_caps_cache
|
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 JoinGroupchatWindow
|
||||||
from gajim.gtk import ErrorDialog
|
from gajim.gtk import ErrorDialog
|
||||||
from gajim.gtk import WarningDialog
|
from gajim.gtk import WarningDialog
|
||||||
|
@ -1794,8 +1794,8 @@ class Interface:
|
||||||
@property
|
@property
|
||||||
def emot_and_basic_re(self):
|
def emot_and_basic_re(self):
|
||||||
if not self._emot_and_basic_re:
|
if not self._emot_and_basic_re:
|
||||||
self._emot_and_basic_re = re.compile(self.emot_and_basic,
|
self._emot_and_basic_re = re.compile(
|
||||||
re.IGNORECASE + re.UNICODE)
|
self.emot_and_basic, re.IGNORECASE)
|
||||||
return self._emot_and_basic_re
|
return self._emot_and_basic_re
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1867,43 +1867,13 @@ class Interface:
|
||||||
basic_pattern += formatting
|
basic_pattern += formatting
|
||||||
self.basic_pattern = basic_pattern
|
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
|
# because emoticons match later (in the string) they need to be after
|
||||||
# basic matches that may occur earlier
|
# basic matches that may occur earlier
|
||||||
self.emot_and_basic = basic_pattern + emoticons_pattern
|
emoticons = emoji_data.get_regex()
|
||||||
|
if app.config.get('ascii_emoticons'):
|
||||||
# needed for xhtml display
|
emoticons += '|%s' % emoji_ascii_data.get_regex()
|
||||||
self.emot_only = emoticons_pattern
|
pass
|
||||||
|
self.emot_and_basic = '%s|%s' % (basic_pattern, emoticons)
|
||||||
|
|
||||||
# at least one character in 3 parts (before @, after @, after .)
|
# at least one character in 3 parts (before @, after @, after .)
|
||||||
self.sth_at_sth_dot_sth = r'\S+@\S+\.\S*[^\s)?]'
|
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]|'\
|
self.invalid_XML_chars = '[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x1f]|'\
|
||||||
'[\ud800-\udfff]|[\ufffe-\uffff]'
|
'[\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
|
### Methods for opening new messages controls
|
||||||
|
@ -2667,7 +2613,6 @@ class Interface:
|
||||||
self.basic_pattern = None
|
self.basic_pattern = None
|
||||||
self.emot_and_basic = None
|
self.emot_and_basic = None
|
||||||
self.sth_at_sth_dot_sth = None
|
self.sth_at_sth_dot_sth = None
|
||||||
self.emot_only = None
|
|
||||||
|
|
||||||
cfg_was_read = parser.read()
|
cfg_was_read = parser.read()
|
||||||
|
|
||||||
|
@ -2814,7 +2759,9 @@ class Interface:
|
||||||
# set the icon to all windows
|
# set the icon to all windows
|
||||||
Gtk.Window.set_default_icon_list(pixs)
|
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()
|
self.make_regexps()
|
||||||
|
|
||||||
# get transports type from DB
|
# get transports type from DB
|
||||||
|
|
|
@ -1081,6 +1081,20 @@ class HtmlTextView(Gtk.TextView):
|
||||||
search_iter.forward_char()
|
search_iter.forward_char()
|
||||||
return selection
|
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
|
change_cursor = None
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -350,6 +350,53 @@ class MessageTextView(Gtk.TextView):
|
||||||
else:
|
else:
|
||||||
return None
|
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):
|
def destroy(self):
|
||||||
GLib.idle_add(gc.collect)
|
GLib.idle_add(gc.collect)
|
||||||
|
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -214,8 +214,7 @@ class update_po(Command):
|
||||||
|
|
||||||
|
|
||||||
package_data_activities = ['data/activities/*/*/*.png']
|
package_data_activities = ['data/activities/*/*/*.png']
|
||||||
package_data_emoticons = ['data/emoticons/*/emoticons_theme.py',
|
package_data_emoticons = ['data/emoticons/*/*.png',
|
||||||
'data/emoticons/*/*.png',
|
|
||||||
'data/emoticons/*/LICENSE']
|
'data/emoticons/*/LICENSE']
|
||||||
package_data_gui = ['data/gui/*.ui']
|
package_data_gui = ['data/gui/*.ui']
|
||||||
package_data_icons = ['data/icons/hicolor/*/*/*.png',
|
package_data_icons = ['data/icons/hicolor/*/*/*.png',
|
||||||
|
|
Loading…
Reference in New Issue