321 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			321 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# -*- 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
 | 
						|
 | 
						|
 |