# Copyright (C) 2005 Gustavo J. A. M. Carneiro
# Copyright (C) 2006 Santiago Gala
# Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
#                    Julien Pivotto <roidelapluie AT gmail.com>
#                    Stephan Erb <steve-e AT h3c.de>
#
# 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/>.

"""
A Gtk.TextView-based renderer for XHTML-IM, as described in:
  http://xmpp.org/extensions/xep-0071.html

Starting with the version posted by Gustavo Carneiro,
I (Santiago Gala) am trying to make it more compatible
with the markup that docutils generate, and also more
modular.
"""

import re
import logging
import xml.sax
import xml.sax.handler
from io import StringIO

from gi.repository import GObject
from gi.repository import Pango
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib

from gajim.common import app
from gajim.common.const import StyleAttr
from gajim.common.helpers import open_uri
from gajim.common.helpers import parse_uri
from gajim.gui_menu_builder import get_conv_context_menu

from .util import get_cursor

log = logging.getLogger('gajim.htmlview')

whitespace_rx = re.compile('\\s+')
allwhitespace_rx = re.compile('^\\s*$')

# embryo of CSS classes
classes = {
    #'system-message':';display: none',
    'problematic': ';color: red',
}

# styles for elements
_element_styles = {
    'u'             : ';text-decoration: underline',
    'em'            : ';font-style: oblique',
    'cite'          : '; background-color:rgb(170,190,250);'
                      'font-style: oblique',
    'li'            : '; margin-left: 1em; margin-right: 10%',
    'strong'        : ';font-weight: bold',
    'pre'           : '; background-color:rgb(190,190,190);'
                      'font-family: monospace; white-space: pre;'
                      'margin-left: 1em; margin-right: 10%',
    'kbd'           : ';background-color:rgb(210,210,210);'
                      'font-family: monospace',
    'blockquote'    : '; background-color:rgb(170,190,250);'
                      'margin-left: 2em; margin-right: 10%',
    'dt'            : ';font-weight: bold; font-style: oblique',
    'dd'            : ';margin-left: 2em; font-style: oblique'
}
# no difference for the moment
_element_styles['dfn'] = _element_styles['em']
_element_styles['var'] = _element_styles['em']
# deprecated, legacy, presentational
_element_styles['tt'] = _element_styles['kbd']
_element_styles['i'] = _element_styles['em']
_element_styles['b'] = _element_styles['strong']

_supported_style_attrs = [
    'background-color', 'color', 'font-family', 'font-size', 'font-style',
    'font-weight', 'margin-left', 'margin-right', 'text-align',
    'text-decoration', 'white-space', 'display', 'width', 'height'
]

# ==========
#   XEP-0071
# ==========
#
# This Integration Set includes a subset of the modules defined for
# XHTML 1.0 but does not redefine any existing modules, nor
# does it define any new modules. Specifically, it includes the
# following modules only:
#
# - Structure
# - Text
#
#   * Block
#
#     phrasal
#        addr, blockquote, pre
#     Struct
#        div,p
#     Heading
#        h1, h2, h3, h4, h5, h6
#
#   * Inline
#
#     phrasal
#        abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
#     structural
#        br, span
#
# - Hypertext (a)
# - List (ul, ol, dl)
# - Image (img)
# - Style Attribute
#
# Therefore XHTML-IM uses the following content models:
#
#   Block.mix
#             Block-like elements, e.g., paragraphs
#   Flow.mix
#             Any block or inline elements
#   Inline.mix
#             Character-level elements
#   InlineNoAnchor.class
#                       Anchor element
#   InlinePre.mix
#             Pre element
#
# XHTML-IM also uses the following Attribute Groups:
#
# Core.extra.attrib
#       TBD
# I18n.extra.attrib
#       TBD
# Common.extra
#       style
#
#
# ...
# block level:
# Heading    h
#            ( head           = h1 | h2 | h3 | h4 | h5 | h6 )
# Block      ( phrasal        = address | blockquote | pre )
# NOT           ( presentational = hr )
#            ( structural     = div | p )
# other:     section
# Inline     ( phrasal        = abbr | acronym | cite | code | dfn | em |
#                               kbd | q | samp | strong | var )
# NOT        ( presentational =  b  | big | i | small | sub | sup | tt )
#            ( structural     =  br | span )
# Param/Legacy    param, font, basefont, center, s, strike, u, dir, menu,
#                 isindex

BLOCK_HEAD = set(('h1', 'h2', 'h3', 'h4', 'h5', 'h6',))
BLOCK_PHRASAL = set(('address', 'blockquote', 'pre',))
BLOCK_PRES = set(('hr', )) #not in xhtml-im
BLOCK_STRUCT = set(('div', 'p', ))
BLOCK_HACKS = set(('table', 'tr')) # at the very least, they will start line ;)
BLOCK = BLOCK_HEAD | BLOCK_PHRASAL | BLOCK_STRUCT | BLOCK_PRES | BLOCK_HACKS

INLINE_PHRASAL = set(['abbr', 'acronym', 'cite', 'code', 'dfn', 'em',
                      'kbd', 'q', 'samp', 'strong', 'var'])
INLINE_PRES = set(['b', 'i', 'u', 'tt']) #not in xhtml-im
INLINE_STRUCT = set(['br', 'span'])
INLINE = INLINE_PHRASAL | INLINE_PRES | INLINE_STRUCT

LIST_ELEMS = set(['dl', 'ol', 'ul'])

for _name in BLOCK_HEAD:
    _num = int(_name[1])
    _header_size = (_num - 1) // 2
    _weight = (_num - 1) % 2
    _element_styles[_name] = '; font-size: %s; %s' % (
        ('large', 'medium', 'small')[_header_size],
        ('font-weight: bold', 'font-style: oblique')[_weight])

def _parse_css_color(color):
    rgba = Gdk.RGBA()
    success = rgba.parse(color)
    if not success:
        log.warning('Can\'t parse color: %s', color)
    return rgba

def style_iter(style):
    for item in style.split(';'):
        if item.strip():
            yield [x.strip() for x in item.split(':', 1)]


class HtmlHandler(xml.sax.handler.ContentHandler):
    """
    A handler to display html to a gtk textview

    It keeps a stack of "style spans" (start/end element pairs) and a stack of
    list counters, for nested lists.
    """
    def __init__(self, textview, conv_textview, startiter):
        xml.sax.handler.ContentHandler.__init__(self)
        self.textbuf = textview.get_buffer()
        self.textview = textview
        self.iter = startiter
        self.conv_textview = conv_textview
        self.text = ''
        self.starting = True
        self.preserve = False
        self.styles = [] # a Gtk.TextTag or None, for each span level
        self.list_counters = [] # stack (top at head) of list
                                # counters, or None for unordered list

        # build a dictionary mapping styles to methods
        self.__style_methods = {}
        for style in _supported_style_attrs:
            method_names = '_parse_style_%s' % style.replace('-', '_')
            self.__style_methods[style] = method_names

    def _get_points_from_pixels(self, pixels):
        resolution = self.textview.get_screen().get_resolution()
        # points = pixels * 72 / resolution
        return pixels * 72 / resolution

    @staticmethod
    def _parse_style_color(tag, value):
        color = _parse_css_color(value)
        tag.set_property('foreground-rgba', color)

    @staticmethod
    def _parse_style_background_color(tag, value):
        color = _parse_css_color(value)
        tag.set_property('background-rgba', color)
        tag.set_property('paragraph-background-rgba', color)

    @staticmethod
    def __parse_length_frac_size_allocate(_textview, allocation, frac,
                                          callback, args):
        callback(allocation.width*frac, *args)

    def _parse_length(self, value, font_relative, block_relative, minl, maxl,
                      callback, *args):
        """
        Parse/calc length, converting to pixels, calls callback(length, *args)
        when the length is first computed or changes
        """
        if value.endswith('%'):
            val = float(value[:-1])
            if val > 0:
                sign = 1
            elif val < 0:
                sign = -1
            else:
                sign = 0
            # limits: 1% to 500%
            val = sign*max(1, min(abs(val), 500))
            frac = val/100
            if font_relative:
                callback(frac, '%', *args)
            elif block_relative:
                # CSS says 'Percentage values: refer to width of the closest
                #           block-level ancestor'
                # This is difficult/impossible to implement, so we use
                # textview width instead; a reasonable approximation..
                alloc = self.textview.get_allocation()
                self.__parse_length_frac_size_allocate(
                    self.textview, alloc, frac, callback, args)
                self.textview.connect('size-allocate',
                                      self.__parse_length_frac_size_allocate,
                                      frac, callback, args)
            else:
                callback(frac, *args)
            return

        def get_val(min_val=minl, max_val=maxl):
            try:
                val = float(value[:-2])
            except Exception:
                log.warning('Unable to parse length value "%s"', value)
                return None
            if val > 0:
                sign = 1
            elif val < 0:
                sign = -1
            else:
                sign = 0
            # validate length
            return sign*max(min_val, min(abs(val), max_val))
        if value.endswith('pt'):  # points
            size = get_val(5, 50)
            if size is None:
                return
            callback(size, 'pt', *args)

        elif value.endswith('em'):
            size = get_val(0.3, 4)
            if size is None:
                return
            callback(size, 'em', *args)

        elif value.endswith('px'):  # pixels
            size = get_val(5, 50)
            if size is None:
                return
            callback(size, 'px', *args)

        else:
            try:
                # TODO: isn't "no units" interpreted as pixels?
                val = int(value)
                if val > 0:
                    sign = 1
                elif val < 0:
                    sign = -1
                else:
                    sign = 0
                # validate length
                val = sign*max(5, min(abs(val), 70))
                callback(val, 'px', *args)
            except Exception:
                log.warning('Unable to parse length value "%s"', value)

    def __parse_font_size_cb(self, size, type_, tag):
        if type_ in ('em', '%'):
            tag.set_property('scale', size)
        elif type_ == 'pt':
            tag.set_property('size-points', size)
        elif type_ == 'px':
            tag.set_property('size-points', self._get_points_from_pixels(size))

    @staticmethod
    def _parse_style_display(tag, value):
        if value == 'none':
            tag.set_property('invisible', 'true')
        # FIXME: display: block, inline

    def _parse_style_font_size(self, tag, value):
        try:
            scale = {
                'xx-small': 0.5787037037037,
                'x-small': 0.6444444444444,
                'small': 0.8333333333333,
                'medium': 1.0,
                'large': 1.2,
                'x-large': 1.4399999999999,
                'xx-large': 1.728,
            }[value]
        except KeyError:
            pass
        else:
            tag.set_property('scale', scale)
            return
        if value == 'smaller':
            tag.set_property('scale', 0.8333333333333)
            return
        if value == 'larger':
            tag.set_property('scale', 1.2)
            return
        # font relative (5 ~ 4pt, 110 ~ 72pt)
        self._parse_length(
            value, True, False, 5, 110, self.__parse_font_size_cb, tag)

    @staticmethod
    def _parse_style_font_style(tag, value):
        try:
            style = {
                'normal': Pango.Style.NORMAL,
                'italic': Pango.Style.ITALIC,
                'oblique': Pango.Style.OBLIQUE,
            }[value]
        except KeyError:
            log.warning('unknown font-style %s', value)
        else:
            tag.set_property('style', style)

    def __frac_length_tag_cb(self, length, tag, propname):
        styles = self._get_style_tags()
        if styles:
            length += styles[-1].get_property(propname)
        tag.set_property(propname, length)

    def _parse_style_margin_left(self, tag, value):
        # block relative
        self._parse_length(value, False, True, 1, 1000,
                           self.__frac_length_tag_cb, tag, 'left-margin')

    def _parse_style_margin_right(self, tag, value):
        # block relative
        self._parse_length(value, False, True, 1, 1000,
                           self.__frac_length_tag_cb, tag, 'right-margin')

    @staticmethod
    def _parse_style_font_weight(tag, value):
        # TODO: missing 'bolder' and 'lighter'
        try:
            weight = {
                '100': Pango.Weight.ULTRALIGHT,
                '200': Pango.Weight.ULTRALIGHT,
                '300': Pango.Weight.LIGHT,
                '400': Pango.Weight.NORMAL,
                '500': Pango.Weight.NORMAL,
                '600': Pango.Weight.BOLD,
                '700': Pango.Weight.BOLD,
                '800': Pango.Weight.ULTRABOLD,
                '900': Pango.Weight.HEAVY,
                'normal': Pango.Weight.NORMAL,
                'bold': Pango.Weight.BOLD,
            }[value]
        except KeyError:
            log.warning('unknown font-style %s', value)
        else:
            tag.set_property('weight', weight)

    @staticmethod
    def _parse_style_font_family(tag, value):
        tag.set_property('family', value)

    @staticmethod
    def _parse_style_text_align(tag, value):
        try:
            align = {
                'left': Gtk.Justification.LEFT,
                'right': Gtk.Justification.RIGHT,
                'center': Gtk.Justification.CENTER,
                'justify': Gtk.Justification.FILL,
            }[value]
        except KeyError:
            log.warning('Invalid text-align: %s requested', value)
        else:
            tag.set_property('justification', align)

    @staticmethod
    def _parse_style_text_decoration(tag, value):
        values = value.split(' ')
        if 'none' in values:
            tag.set_property('underline', Pango.Underline.NONE)
            tag.set_property('strikethrough', False)
        if 'underline' in values:
            tag.set_property('underline', Pango.Underline.SINGLE)
        else:
            tag.set_property('underline', Pango.Underline.NONE)
        if 'line-through' in values:
            tag.set_property('strikethrough', True)
        else:
            tag.set_property('strikethrough', False)
        if 'blink' in values:
            log.warning('text-decoration:blink not implemented')
        if 'overline' in values:
            log.warning('text-decoration:overline not implemented')

    @staticmethod
    def _parse_style_white_space(tag, value):
        if value == 'pre':
            tag.set_property('wrap_mode', Gtk.WrapMode.NONE)
        elif value == 'normal':
            tag.set_property('wrap_mode', Gtk.WrapMode.WORD)
        elif value == 'nowrap':
            tag.set_property('wrap_mode', Gtk.WrapMode.NONE)

    @staticmethod
    def __length_tag_cb(value, tag, propname):
        try:
            tag.set_property(propname, value)
        except Exception:
            log.warning('Error with prop: %s for tag: %s', propname, str(tag))

    def _parse_style_width(self, tag, value):
        if value == 'auto':
            return
        self._parse_length(value, False, False, 1, 1000,
                           self.__length_tag_cb, tag, "width")
    def _parse_style_height(self, tag, value):
        if value == 'auto':
            return
        self._parse_length(value, False, False, 1, 1000,
                           self.__length_tag_cb, tag, "height")

    def _get_style_tags(self):
        return [tag for tag in self.styles if tag is not None]

    def _create_url(self, href, title, type_, id_):
        '''Process a url tag.
        '''
        tag = self.textbuf.create_tag(id_)
        if href and href[0] != '#':
            tag.href = href
            tag.type_ = type_ # to be used by the URL handler
            tag.connect('event', self.textview.hyperlink_handler, 'url')
            tag.set_property('foreground',
                             app.css_config.get_value('.gajim-url',
                                                      StyleAttr.COLOR))
            tag.set_property('underline', Pango.Underline.SINGLE)
            tag.is_anchor = True
        if title:
            tag.title = title
        return tag

    def _begin_span(self, style, tag=None, id_=None):
        if style is None:
            self.styles.append(tag)
            return
        if tag is None:
            if id_:
                tag = self.textbuf.create_tag(id_)
            else:
                tag = self.textbuf.create_tag() # we create anonymous tag
        for attr, val in style_iter(style):
            attr = attr.lower()
            try:
                getattr(self, self.__style_methods[attr])(tag, val)
            except KeyError:
                log.warning('Style attribute "%s" requested '
                            'but not yet implemented', attr)
        self.styles.append(tag)

    def _end_span(self):
        self.styles.pop()

    def _jump_line(self):
        self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
        self.starting = True

    def _insert_text(self, text, working_iter=None):
        if working_iter is None:
            working_iter = self.iter
        if self.starting and text != '\n':
            self.starting = (text[-1] == '\n')
        tags = self._get_style_tags()
        if tags:
            self.textbuf.insert_with_tags(working_iter, text, *tags)
        else:
            self.textbuf.insert(working_iter, text)

    def _starts_line(self):
        return self.starting or self.iter.starts_line()

    def _flush_text(self):
        if not self.text:
            return
        text, self.text = self.text, ''
        if not self.preserve:
            text = text.replace('\n', ' ')
            self.handle_specials(whitespace_rx.sub(' ', text))
        else:
            self._insert_text(text.strip('\n'))

    def _anchor_event(self, _tag, _textview, event, _iter, href, type_):
        if event.type == Gdk.EventType.BUTTON_PRESS:
            self.textview.emit('url-clicked', href, type_)
            return True
        return False

    def handle_specials(self, text):
        if self.conv_textview:
            self.iter = self.conv_textview.detect_and_print_special_text(
                text, self._get_style_tags(), iter_=self.iter)
        else:
            self._insert_text(text)

    def characters(self, content):
        if self.preserve:
            self.text += content
            return
        if allwhitespace_rx.match(content) is not None and self._starts_line():
            return
        self.text += content
        self.starting = False

    def startElement(self, name, attrs):
        self._flush_text()
        klass = [i for i in attrs.get('class', ' ').split(' ') if i]
        style = ''
        #Add styles defined for classes
        for k in klass:
            if k  in classes:
                style += classes[k]

        tag = None
        #FIXME: if we want to use id, it needs to be unique across
        # the whole textview, so we need to add something like the
        # message-id to it.
        #id_ = attrs.get('id',None)
        id_ = None
        if name == 'a':
            #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
            href = attrs.get('href', None)
            if not href:
                href = attrs.get('HREF', None)
            # Gaim sends HREF instead of href
            title = attrs.get('title', attrs.get('rel', href))
            type_ = attrs.get('type', None)
            tag = self._create_url(href, title, type_, id_)
        elif name == 'blockquote':
            cite = attrs.get('cite', None)
            if cite:
                tag = self.textbuf.create_tag(id_)
                tag.title = attrs.get('title', None)
                tag.is_anchor = True
        elif name in LIST_ELEMS:
            style += ';margin-left: 2em'

        if name in _element_styles:
            style += _element_styles[name]
        # so that explicit styles override implicit ones,
        # we add the attribute last
        style += ";"+attrs.get('style', '')
        if style == '':
            style = None
        self._begin_span(style, tag, id_)

        if name == 'br':
            pass # handled in endElement
        elif name == 'hr':
            pass # handled in endElement
        elif name in BLOCK:
            if not self._starts_line():
                self._jump_line()
            if name == 'pre':
                self.preserve = True
        elif name == 'span':
            pass
        elif name in ('dl', 'ul'):
            if not self._starts_line():
                self._jump_line()
            self.list_counters.append(None)
        elif name == 'ol':
            if not self._starts_line():
                self._jump_line()
            self.list_counters.append(0)
        elif name == 'li':
            if self.list_counters[-1] is None:
                li_head = chr(0x2022)
            else:
                self.list_counters[-1] += 1
                li_head = '%i.' % self.list_counters[-1]
            self.text = ' '*len(self.list_counters)*4 + li_head + ' '
            self._flush_text()
            self.starting = True
        elif name == 'dd':
            self._jump_line()
        elif name == 'dt':
            if not self.starting:
                self._jump_line()
        elif name in ('a', 'img', 'body', 'html'):
            pass
        elif name in INLINE:
            pass
        else:
            log.warning('Unhandled element "%s"', name)

    def endElement(self, name):
        end_preserving = False
        newline = False
        if name == 'br':
            newline = True
        elif name == 'hr':
            #FIXME: plenty of unused attributes (width, height,...) :)
            self._jump_line()
            self._insert_text('\u2015'*40)
            self._jump_line()
        elif name in LIST_ELEMS:
            self.list_counters.pop()
        elif name == 'li':
            newline = True
        elif name == 'img':
            pass
        elif name in ('body', 'html'):
            pass
        elif name == 'a':
            pass
        elif name in INLINE:
            pass
        elif name in ('dd', 'dt', ):
            pass
        elif name in BLOCK:
            if name == 'pre':
                end_preserving = True
            elif name in BLOCK_STRUCT:
                newline = True
        else:
            log.warning("Unhandled element '%s'", name)
        self._flush_text()
        if end_preserving:
            self.preserve = False
        if newline:
            self._jump_line()
        self._end_span()


class HtmlTextView(Gtk.TextView):

    _tags = ['url', 'mail', 'xmpp', 'sth_at_sth']

    def __init__(self, account, standalone=False):
        Gtk.TextView.__init__(self)

        self.set_has_tooltip(True)
        self.set_border_width(1)
        self.set_accepts_tab(True)
        self.set_editable(False)
        self.set_cursor_visible(False)
        self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
        self.set_left_margin(2)
        self.set_right_margin(2)

        self.drag_dest_unset()

        self.connect('copy-clipboard', self._on_copy_clipboard)
        self.connect('destroy', self._on_destroy)
        self.get_buffer().eol_tag = self.get_buffer().create_tag('eol')

        self.account = account
        self.plugin_modified = False
        self._cursor_changed = False

        if standalone:
            self.connect('query-tooltip', self._query_tooltip)
            self.create_tags()

    def _on_destroy(self, *args):
        # We restore the TextView’s drag destination to avoid a GTK warning
        # when closing the control. ChatControlBase.shutdown() calls destroy()
        # on the control’s main box, causing GTK to recursively destroy the
        # child widgets. GTK then tries to set a target list on the TextView,
        # resulting in a warning because the Widget has no drag destination.
        self.drag_dest_set(
            Gtk.DestDefaults.ALL,
            None,
            Gdk.DragAction.DEFAULT)

    def create_tags(self):
        color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR)

        attrs = {'foreground': color,
                 'underline': Pango.Underline.SINGLE}

        for tag in self._tags:
            tag_ = self.get_buffer().create_tag(tag, **attrs)
            tag_.connect('event', self.hyperlink_handler, tag)

    def update_tags(self):
        tag_table = self.get_buffer().get_tag_table()
        color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR)

        for tag in self._tags:
            tag_table.lookup(tag).set_property('foreground', color)

    def _query_tooltip(self, widget, x_pos, y_pos, _keyboard_mode, tooltip):
        window = widget.get_window(Gtk.TextWindowType.TEXT)
        x_pos, y_pos = self.window_to_buffer_coords(
            Gtk.TextWindowType.TEXT, x_pos, y_pos)
        iter_ = self.get_iter_at_position(x_pos, y_pos)[1]

        for tag in iter_.get_tags():
            if getattr(tag, 'is_anchor', False):
                text = getattr(tag, 'title', False)
                if text:
                    if len(text) > 50:
                        text = text[:47] + '…'
                    tooltip.set_text(text)
                    window.set_cursor(get_cursor('pointer'))
                    self._cursor_changed = True
                    return True

            tag_name = tag.get_property('name')
            if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'):
                window.set_cursor(get_cursor('pointer'))
                self._cursor_changed = True
                return False

        if self._cursor_changed:
            window.set_cursor(get_cursor('text'))
            self._cursor_changed = False
        return False

    def show_context_menu(self, uri):
        menu = get_conv_context_menu(self.account, uri)
        if menu is None:
            log.warning('No handler for URI type: %s', uri)
            return

        def destroy(menu, _pspec):
            visible = menu.get_property('visible')
            if not visible:
                GLib.idle_add(menu.destroy)

        menu.attach_to_widget(self, None)
        menu.connect('notify::visible', destroy)
        menu.popup_at_pointer()

    def hyperlink_handler(self, texttag, _widget, event, iter_, _kind):
        if event.type != Gdk.EventType.BUTTON_PRESS:
            return Gdk.EVENT_PROPAGATE

        begin_iter = iter_.copy()
        # we get the beginning of the tag
        while not begin_iter.starts_tag(texttag):
            begin_iter.backward_char()
        end_iter = iter_.copy()
        # we get the end of the tag
        while not end_iter.ends_tag(texttag):
            end_iter.forward_char()

        # Detect XHTML-IM link
        word = getattr(texttag, 'href', None)
        if not word:
            word = self.get_buffer().get_text(begin_iter, end_iter, True)

        uri = parse_uri(word)
        if event.button.button == 3: # right click
            self.show_context_menu(uri)
            return Gdk.EVENT_STOP

        self.plugin_modified = False
        app.plugin_manager.extension_point(
            'hyperlink_handler', uri, self, self.get_toplevel())
        if self.plugin_modified:
            return Gdk.EVENT_STOP

        open_uri(uri, account=self.account)
        return Gdk.EVENT_STOP

    def display_html(self, html, textview, conv_textview, iter_=None):
        buffer_ = self.get_buffer()
        if iter_:
            eob = iter_
        else:
            eob = buffer_.get_end_iter()
        parser = xml.sax.make_parser()
        parser.setContentHandler(HtmlHandler(textview, conv_textview, eob))
        parser.parse(StringIO(html))
        # If the xhtml ends with a BLOCK element we have to remove
        # the \n we add after BLOCK elements
        self._delete_last_char(buffer_, eob)

    @staticmethod
    def _delete_last_char(buffer_, iter_):
        start_iter = iter_.copy()
        start_iter.backward_char()
        text = buffer_.get_text(start_iter, iter_, True)
        if text == '\n':
            buffer_.delete(start_iter, iter_)

    @staticmethod
    def _on_copy_clipboard(textview):
        clipboard = textview.get_clipboard(Gdk.SELECTION_CLIPBOARD)
        selected = textview.get_selected_text()
        clipboard.set_text(selected, -1)
        GObject.signal_stop_emission_by_name(textview, 'copy-clipboard')

    def get_selected_text(self):
        bounds = self.get_buffer().get_selection_bounds()
        selection = ''
        if bounds:
            (search_iter, end) = bounds

            while search_iter.compare(end):
                character = search_iter.get_char()
                if character == '\ufffc':
                    anchor = search_iter.get_child_anchor()
                    if anchor:
                        text = anchor.plaintext
                        if text:
                            selection += text
                    else:
                        selection += character
                else:
                    selection += character
                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)

    def unselect(self):
        buffer_ = self.get_buffer()
        insert_iter = buffer_.get_iter_at_mark(buffer_.get_insert())
        buffer_.select_range(insert_iter, insert_iter)
