# -*- coding:utf-8 -*- # # Copyright (C) 2017 Philipp Hörist # # 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 . import os import sys import logging import importlib.util as imp 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 dont 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