Daniel Brötzmann pushed to branch master at gajim / gajim

Commits:

3 changed files:

Changes:

  • gajim/gtk/conversation/plain_widget.py
    ... ... @@ -21,6 +21,7 @@
    21 21
     from gi.repository import Gtk
    
    22 22
     from gi.repository import Pango
    
    23 23
     from gi.repository import Gdk
    
    24
    +from gi.repository import GdkPixbuf
    
    24 25
     from gi.repository import GLib
    
    25 26
     from gi.repository import GObject
    
    26 27
     
    
    ... ... @@ -65,7 +66,7 @@ def add_content(self, block: PlainBlock) -> None:
    65 66
             self._text_widget.print_text_with_styling(block)
    
    66 67
     
    
    67 68
         def add_action_phrase(self, text: str, nickname: str) -> None:
    
    68
    -        text = text.replace('/me', '* %s' % nickname, 1)
    
    69
    +        text = text.replace('/me', f'* {nickname}', 1)
    
    69 70
             text = GLib.markup_escape_text(text)
    
    70 71
             self._text_widget.add_action_phrase(text)
    
    71 72
     
    
    ... ... @@ -255,6 +256,25 @@ def print_text_with_styling(self, block: PlainBlock) -> None:
    255 256
                     emoji_pixbufs.append_marks(
    
    256 257
                         self, start_mark, end_mark, emoji.text)
    
    257 258
     
    
    259
    +    def replace_emoji(self,
    
    260
    +                      start_mark: Gtk.TextMark,
    
    261
    +                      end_mark: Gtk.TextMark,
    
    262
    +                      pixbuf: GdkPixbuf.Pixbuf,
    
    263
    +                      codepoint: str
    
    264
    +                      ) -> None:
    
    265
    +        buffer_ = self.get_buffer()
    
    266
    +        start_iter = buffer_.get_iter_at_mark(start_mark)
    
    267
    +        end_iter = buffer_.get_iter_at_mark(end_mark)
    
    268
    +        buffer_.delete(start_iter, end_iter)
    
    269
    +
    
    270
    +        anchor = buffer_.create_child_anchor(start_iter)
    
    271
    +        anchor.plaintext = codepoint  # type: ignore
    
    272
    +        img = Gtk.Image.new_from_pixbuf(pixbuf)
    
    273
    +        img.show()
    
    274
    +        self.add_child_at_anchor(img, anchor)
    
    275
    +        buffer_.delete_mark(start_mark)
    
    276
    +        buffer_.delete_mark(end_mark)
    
    277
    +
    
    258 278
         def add_action_phrase(self, text: str) -> None:
    
    259 279
             buffer_ = self.get_buffer()
    
    260 280
             buffer_.insert(buffer_.get_start_iter(), text.strip())
    
    ... ... @@ -268,7 +288,7 @@ def _query_tooltip(self,
    268 288
                            x_pos: int,
    
    269 289
                            y_pos: int,
    
    270 290
                            _keyboard_mode: bool,
    
    271
    -                       tooltip: Gtk.Tooltip
    
    291
    +                       _tooltip: Gtk.Tooltip
    
    272 292
                            ) -> bool:
    
    273 293
     
    
    274 294
             window = widget.get_window(Gtk.TextWindowType.TEXT)
    

  • gajim/gtk/emoji_data.py
    ... ... @@ -14,12 +14,15 @@
    14 14
     
    
    15 15
     # pylint: disable=too-many-lines
    
    16 16
     
    
    17
    +from typing import Optional
    
    18
    +
    
    17 19
     import re
    
    18 20
     import weakref
    
    19 21
     from enum import IntEnum
    
    20 22
     from collections import OrderedDict
    
    21 23
     
    
    22 24
     from gi.repository import GdkPixbuf
    
    25
    +from gi.repository import Gtk
    
    23 26
     
    
    24 27
     _MODIFIERS = ['\U0001F3FB',
    
    25 28
                   '\U0001F3FC',
    
    ... ... @@ -36,7 +39,7 @@ class Emoji(IntEnum):
    36 39
         PARSE_COLUMNS = 20
    
    37 40
     
    
    38 41
     
    
    39
    -def is_emoji(codepoints):
    
    42
    +def is_emoji(codepoints: str) -> bool:
    
    40 43
         if codepoints in emoji_data:
    
    41 44
             return True
    
    42 45
         if codepoints in emoji_ascii_data:
    
    ... ... @@ -44,7 +47,7 @@ def is_emoji(codepoints):
    44 47
         return False
    
    45 48
     
    
    46 49
     
    
    47
    -def get_emoji_pixbuf(codepoints):
    
    50
    +def get_emoji_pixbuf(codepoints: str) -> Optional[GdkPixbuf.Pixbuf]:
    
    48 51
         ascii_codepoint = emoji_ascii_data.get(codepoints, None)
    
    49 52
         if ascii_codepoint is not None:
    
    50 53
             codepoints = ascii_codepoint
    
    ... ... @@ -75,7 +78,7 @@ def __contains__(self, item):
    75 78
             except KeyError:
    
    76 79
                 return False
    
    77 80
     
    
    78
    -    def get_regex(self):
    
    81
    +    def get_regex(self) -> str:
    
    79 82
             emojis = []
    
    80 83
             for codepoint, attrs in self.items():
    
    81 84
                 if attrs.get('variations', False):
    
    ... ... @@ -92,38 +95,49 @@ class EmojiPixbufs(dict):
    92 95
     
    
    93 96
         def __init__(self):
    
    94 97
             dict.__init__(self)
    
    95
    -        self._marks = weakref.WeakKeyDictionary()
    
    98
    +        self._marks: weakref.WeakKeyDictionary[
    
    99
    +            Gtk.TextView,
    
    100
    +            list[tuple[
    
    101
    +                weakref.ReferenceType[Gtk.TextMark],
    
    102
    +                weakref.ReferenceType[Gtk.TextMark],
    
    103
    +                str]]
    
    104
    +            ] = weakref.WeakKeyDictionary()
    
    96 105
     
    
    97 106
         @property
    
    98
    -    def complete(self):
    
    107
    +    def complete(self) -> bool:
    
    99 108
             return self._complete
    
    100 109
     
    
    101 110
         @complete.setter
    
    102
    -    def complete(self, value):
    
    103
    -        if value is True:
    
    111
    +    def complete(self, value: bool) -> None:
    
    112
    +        if value:
    
    104 113
                 self._replace_emojis()
    
    105 114
                 self._marks.clear()
    
    106 115
             self._complete = value
    
    107 116
     
    
    108
    -    def clear(self):
    
    117
    +    def clear(self) -> None:
    
    109 118
             self.complete = False
    
    110 119
             self._marks.clear()
    
    111 120
             dict.clear(self)
    
    112 121
     
    
    113
    -    def append_marks(self, textview, start, end, codepoint):
    
    122
    +    def append_marks(self,
    
    123
    +                     textview: Gtk.TextView,
    
    124
    +                     start: Gtk.TextMark,
    
    125
    +                     end: Gtk.TextMark,
    
    126
    +                     codepoint: str
    
    127
    +                     ) -> None:
    
    114 128
             # We have to assign some dummy data to the mark, or else
    
    115 129
             # pygobject will not keep the python wrapper alive, which in turn
    
    116 130
             # makes the weakref invalid as soon as the method ends
    
    117
    -        start.dummy = 'x'
    
    118
    -        start = weakref.ref(start)
    
    119
    -        end.dummy = 'x'
    
    120
    -        end = weakref.ref(end)
    
    131
    +        start.dummy = 'x'  # type: ignore
    
    132
    +        start_ref = weakref.ref(start)
    
    133
    +        end.dummy = 'x'  # type: ignore
    
    134
    +        end_ref = weakref.ref(end)
    
    121 135
             if textview in self._marks:
    
    122
    -            self._marks[textview].append((start, end, codepoint))
    
    136
    +            self._marks[textview].append((start_ref, end_ref, codepoint))
    
    123 137
             else:
    
    124
    -            self._marks[textview] = [(start, end, codepoint)]
    
    138
    +            self._marks[textview] = [(start_ref, end_ref, codepoint)]
    
    125 139
     
    
    126
    -    def _replace_emojis(self):
    
    140
    +    def _replace_emojis(self) -> None:
    
    127 141
             for textview, emojis in self._marks.items():
    
    128 142
                 for emoji in emojis:
    
    129 143
                     start, end, codepoint = emoji
    
    ... ... @@ -140,7 +154,7 @@ def _replace_emojis(self):
    140 154
                     pixbuf = pixbuf.scale_simple(Emoji.TEXT_SIZE,
    
    141 155
                                                  Emoji.TEXT_SIZE,
    
    142 156
                                                  GdkPixbuf.InterpType.HYPER)
    
    143
    -                textview.replace_emojis(
    
    157
    +                textview.replace_emoji(
    
    144 158
                         start(), end(), pixbuf.copy(), codepoint)
    
    145 159
     
    
    146 160
     
    
    ... ... @@ -148,7 +162,7 @@ def _replace_emojis(self):
    148 162
     
    
    149 163
     # pylint: disable=line-too-long
    
    150 164
     
    
    151
    -emoji_ascii_data = dict([
    
    165
    +emoji_ascii_data: dict[str, str] = dict([
    
    152 166
         ("':-D", '\U0001F605'),
    
    153 167
         (':)', '\U0001F642'),
    
    154 168
         (':-O', '\U0001F62e'),
    

  • gajim/gtk/htmltextview.py deleted
    1
    -# Copyright (C) 2005 Gustavo J. A. M. Carneiro
    
    2
    -# Copyright (C) 2006 Santiago Gala
    
    3
    -# Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
    
    4
    -# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
    
    5
    -# Copyright (C) 2007 Nikos Kouremenos <kourem AT gmail.com>
    
    6
    -# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
    
    7
    -#                    Julien Pivotto <roidelapluie AT gmail.com>
    
    8
    -#                    Stephan Erb <steve-e AT h3c.de>
    
    9
    -#
    
    10
    -# This file is part of Gajim.
    
    11
    -#
    
    12
    -# Gajim is free software; you can redistribute it and/or modify
    
    13
    -# it under the terms of the GNU General Public License as published
    
    14
    -# by the Free Software Foundation; version 3 only.
    
    15
    -#
    
    16
    -# Gajim is distributed in the hope that it will be useful,
    
    17
    -# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    18
    -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    19
    -# GNU General Public License for more details.
    
    20
    -#
    
    21
    -# You should have received a copy of the GNU General Public License
    
    22
    -# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    23
    -
    
    24
    -"""
    
    25
    -A Gtk.TextView-based renderer for XHTML-IM, as described in:
    
    26
    -  http://xmpp.org/extensions/xep-0071.html
    
    27
    -
    
    28
    -Starting with the version posted by Gustavo Carneiro,
    
    29
    -I (Santiago Gala) am trying to make it more compatible
    
    30
    -with the markup that docutils generate, and also more
    
    31
    -modular.
    
    32
    -"""
    
    33
    -
    
    34
    -import re
    
    35
    -import logging
    
    36
    -import xml.sax
    
    37
    -import xml.sax.handler
    
    38
    -from io import StringIO
    
    39
    -
    
    40
    -from gi.repository import GObject
    
    41
    -from gi.repository import Pango
    
    42
    -from gi.repository import Gtk
    
    43
    -from gi.repository import Gdk
    
    44
    -from gi.repository import GLib
    
    45
    -
    
    46
    -from gajim.common import app
    
    47
    -from gajim.common.const import StyleAttr
    
    48
    -from gajim.common.helpers import open_uri
    
    49
    -from gajim.common.helpers import parse_uri
    
    50
    -
    
    51
    -from .menus import get_conv_context_menu
    
    52
    -from .util import get_cursor
    
    53
    -
    
    54
    -log = logging.getLogger('gajim.htmlview')
    
    55
    -
    
    56
    -whitespace_rx = re.compile('\\s+')
    
    57
    -allwhitespace_rx = re.compile('^\\s*$')
    
    58
    -
    
    59
    -# embryo of CSS classes
    
    60
    -classes = {
    
    61
    -    #'system-message':';display: none',
    
    62
    -    'problematic': ';color: red',
    
    63
    -}
    
    64
    -
    
    65
    -# styles for elements
    
    66
    -_element_styles = {
    
    67
    -    'u'             : ';text-decoration: underline',
    
    68
    -    'em'            : ';font-style: oblique',
    
    69
    -    'cite'          : '; background-color:rgb(170,190,250);'
    
    70
    -                      'font-style: oblique',
    
    71
    -    'li'            : '; margin-left: 1em; margin-right: 10%',
    
    72
    -    'strong'        : ';font-weight: bold',
    
    73
    -    'pre'           : '; background-color:rgb(190,190,190);'
    
    74
    -                      'font-family: monospace; white-space: pre;'
    
    75
    -                      'margin-left: 1em; margin-right: 10%',
    
    76
    -    'kbd'           : ';background-color:rgb(210,210,210);'
    
    77
    -                      'font-family: monospace',
    
    78
    -    'blockquote'    : '; background-color:rgb(170,190,250);'
    
    79
    -                      'margin-left: 2em; margin-right: 10%',
    
    80
    -    'dt'            : ';font-weight: bold; font-style: oblique',
    
    81
    -    'dd'            : ';margin-left: 2em; font-style: oblique'
    
    82
    -}
    
    83
    -# no difference for the moment
    
    84
    -_element_styles['dfn'] = _element_styles['em']
    
    85
    -_element_styles['var'] = _element_styles['em']
    
    86
    -# deprecated, legacy, presentational
    
    87
    -_element_styles['tt'] = _element_styles['kbd']
    
    88
    -_element_styles['i'] = _element_styles['em']
    
    89
    -_element_styles['b'] = _element_styles['strong']
    
    90
    -
    
    91
    -_supported_style_attrs = [
    
    92
    -    'background-color', 'color', 'font-family', 'font-size', 'font-style',
    
    93
    -    'font-weight', 'margin-left', 'margin-right', 'text-align',
    
    94
    -    'text-decoration', 'white-space', 'display', 'width', 'height'
    
    95
    -]
    
    96
    -
    
    97
    -# ==========
    
    98
    -#   XEP-0071
    
    99
    -# ==========
    
    100
    -#
    
    101
    -# This Integration Set includes a subset of the modules defined for
    
    102
    -# XHTML 1.0 but does not redefine any existing modules, nor
    
    103
    -# does it define any new modules. Specifically, it includes the
    
    104
    -# following modules only:
    
    105
    -#
    
    106
    -# - Structure
    
    107
    -# - Text
    
    108
    -#
    
    109
    -#   * Block
    
    110
    -#
    
    111
    -#     phrasal
    
    112
    -#        addr, blockquote, pre
    
    113
    -#     Struct
    
    114
    -#        div,p
    
    115
    -#     Heading
    
    116
    -#        h1, h2, h3, h4, h5, h6
    
    117
    -#
    
    118
    -#   * Inline
    
    119
    -#
    
    120
    -#     phrasal
    
    121
    -#        abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
    
    122
    -#     structural
    
    123
    -#        br, span
    
    124
    -#
    
    125
    -# - Hypertext (a)
    
    126
    -# - List (ul, ol, dl)
    
    127
    -# - Image (img)
    
    128
    -# - Style Attribute
    
    129
    -#
    
    130
    -# Therefore XHTML-IM uses the following content models:
    
    131
    -#
    
    132
    -#   Block.mix
    
    133
    -#             Block-like elements, e.g., paragraphs
    
    134
    -#   Flow.mix
    
    135
    -#             Any block or inline elements
    
    136
    -#   Inline.mix
    
    137
    -#             Character-level elements
    
    138
    -#   InlineNoAnchor.class
    
    139
    -#                       Anchor element
    
    140
    -#   InlinePre.mix
    
    141
    -#             Pre element
    
    142
    -#
    
    143
    -# XHTML-IM also uses the following Attribute Groups:
    
    144
    -#
    
    145
    -# Core.extra.attrib
    
    146
    -#       TBD
    
    147
    -# I18n.extra.attrib
    
    148
    -#       TBD
    
    149
    -# Common.extra
    
    150
    -#       style
    
    151
    -#
    
    152
    -#
    
    153
    -# ...
    
    154
    -# block level:
    
    155
    -# Heading    h
    
    156
    -#            ( head           = h1 | h2 | h3 | h4 | h5 | h6 )
    
    157
    -# Block      ( phrasal        = address | blockquote | pre )
    
    158
    -# NOT           ( presentational = hr )
    
    159
    -#            ( structural     = div | p )
    
    160
    -# other:     section
    
    161
    -# Inline     ( phrasal        = abbr | acronym | cite | code | dfn | em |
    
    162
    -#                               kbd | q | samp | strong | var )
    
    163
    -# NOT        ( presentational =  b  | big | i | small | sub | sup | tt )
    
    164
    -#            ( structural     =  br | span )
    
    165
    -# Param/Legacy    param, font, basefont, center, s, strike, u, dir, menu,
    
    166
    -#                 isindex
    
    167
    -
    
    168
    -BLOCK_HEAD = set(('h1', 'h2', 'h3', 'h4', 'h5', 'h6',))
    
    169
    -BLOCK_PHRASAL = set(('address', 'blockquote', 'pre',))
    
    170
    -BLOCK_PRES = set(('hr', )) #not in xhtml-im
    
    171
    -BLOCK_STRUCT = set(('div', 'p', ))
    
    172
    -BLOCK_HACKS = set(('table', 'tr')) # at the very least, they will start line ;)
    
    173
    -BLOCK = BLOCK_HEAD | BLOCK_PHRASAL | BLOCK_STRUCT | BLOCK_PRES | BLOCK_HACKS
    
    174
    -
    
    175
    -INLINE_PHRASAL = set(['abbr', 'acronym', 'cite', 'code', 'dfn', 'em',
    
    176
    -                      'kbd', 'q', 'samp', 'strong', 'var'])
    
    177
    -INLINE_PRES = set(['b', 'i', 'u', 'tt']) #not in xhtml-im
    
    178
    -INLINE_STRUCT = set(['br', 'span'])
    
    179
    -INLINE = INLINE_PHRASAL | INLINE_PRES | INLINE_STRUCT
    
    180
    -
    
    181
    -LIST_ELEMS = set(['dl', 'ol', 'ul'])
    
    182
    -
    
    183
    -for _name in BLOCK_HEAD:
    
    184
    -    _num = int(_name[1])
    
    185
    -    _header_size = (_num - 1) // 2
    
    186
    -    _weight = (_num - 1) % 2
    
    187
    -    _element_styles[_name] = '; font-size: %s; %s' % (
    
    188
    -        ('large', 'medium', 'small')[_header_size],
    
    189
    -        ('font-weight: bold', 'font-style: oblique')[_weight])
    
    190
    -
    
    191
    -def _parse_css_color(color):
    
    192
    -    rgba = Gdk.RGBA()
    
    193
    -    success = rgba.parse(color)
    
    194
    -    if not success:
    
    195
    -        log.warning('Can\'t parse color: %s', color)
    
    196
    -    return rgba
    
    197
    -
    
    198
    -def style_iter(style):
    
    199
    -    for item in style.split(';'):
    
    200
    -        if item.strip():
    
    201
    -            yield [x.strip() for x in item.split(':', 1)]
    
    202
    -
    
    203
    -
    
    204
    -class HtmlHandler(xml.sax.handler.ContentHandler):
    
    205
    -    """
    
    206
    -    A handler to display html to a gtk textview
    
    207
    -
    
    208
    -    It keeps a stack of "style spans" (start/end element pairs) and a stack of
    
    209
    -    list counters, for nested lists.
    
    210
    -    """
    
    211
    -    def __init__(self, textview, startiter):
    
    212
    -        xml.sax.handler.ContentHandler.__init__(self)
    
    213
    -        self.textbuf = textview.get_buffer()
    
    214
    -        self.textview = textview
    
    215
    -        self.iter = startiter
    
    216
    -        self.text = ''
    
    217
    -        self.starting = True
    
    218
    -        self.preserve = False
    
    219
    -        self.styles = [] # a Gtk.TextTag or None, for each span level
    
    220
    -        self.list_counters = [] # stack (top at head) of list
    
    221
    -                                # counters, or None for unordered list
    
    222
    -
    
    223
    -        # build a dictionary mapping styles to methods
    
    224
    -        self.__style_methods = {}
    
    225
    -        for style in _supported_style_attrs:
    
    226
    -            method_names = '_parse_style_%s' % style.replace('-', '_')
    
    227
    -            self.__style_methods[style] = method_names
    
    228
    -
    
    229
    -    def _get_points_from_pixels(self, pixels):
    
    230
    -        resolution = self.textview.get_screen().get_resolution()
    
    231
    -        # points = pixels * 72 / resolution
    
    232
    -        return pixels * 72 / resolution
    
    233
    -
    
    234
    -    @staticmethod
    
    235
    -    def _parse_style_color(tag, value):
    
    236
    -        color = _parse_css_color(value)
    
    237
    -        tag.set_property('foreground-rgba', color)
    
    238
    -
    
    239
    -    @staticmethod
    
    240
    -    def _parse_style_background_color(tag, value):
    
    241
    -        color = _parse_css_color(value)
    
    242
    -        tag.set_property('background-rgba', color)
    
    243
    -        tag.set_property('paragraph-background-rgba', color)
    
    244
    -
    
    245
    -    @staticmethod
    
    246
    -    def __parse_length_frac_size_allocate(_textview, allocation, frac,
    
    247
    -                                          callback, args):
    
    248
    -        callback(allocation.width*frac, *args)
    
    249
    -
    
    250
    -    def _parse_length(self, value, font_relative, block_relative, minl, maxl,
    
    251
    -                      callback, *args):
    
    252
    -        """
    
    253
    -        Parse/calc length, converting to pixels, calls callback(length, *args)
    
    254
    -        when the length is first computed or changes
    
    255
    -        """
    
    256
    -        if value.endswith('%'):
    
    257
    -            val = float(value[:-1])
    
    258
    -            if val > 0:
    
    259
    -                sign = 1
    
    260
    -            elif val < 0:
    
    261
    -                sign = -1
    
    262
    -            else:
    
    263
    -                sign = 0
    
    264
    -            # limits: 1% to 500%
    
    265
    -            val = sign*max(1, min(abs(val), 500))
    
    266
    -            frac = val/100
    
    267
    -            if font_relative:
    
    268
    -                callback(frac, '%', *args)
    
    269
    -            elif block_relative:
    
    270
    -                # CSS says 'Percentage values: refer to width of the closest
    
    271
    -                #           block-level ancestor'
    
    272
    -                # This is difficult/impossible to implement, so we use
    
    273
    -                # textview width instead; a reasonable approximation..
    
    274
    -                alloc = self.textview.get_allocation()
    
    275
    -                self.__parse_length_frac_size_allocate(
    
    276
    -                    self.textview, alloc, frac, callback, args)
    
    277
    -                self.textview.connect('size-allocate',
    
    278
    -                                      self.__parse_length_frac_size_allocate,
    
    279
    -                                      frac, callback, args)
    
    280
    -            else:
    
    281
    -                callback(frac, *args)
    
    282
    -            return
    
    283
    -
    
    284
    -        def get_val(min_val=minl, max_val=maxl):
    
    285
    -            try:
    
    286
    -                val = float(value[:-2])
    
    287
    -            except Exception:
    
    288
    -                log.warning('Unable to parse length value "%s"', value)
    
    289
    -                return None
    
    290
    -            if val > 0:
    
    291
    -                sign = 1
    
    292
    -            elif val < 0:
    
    293
    -                sign = -1
    
    294
    -            else:
    
    295
    -                sign = 0
    
    296
    -            # validate length
    
    297
    -            return sign*max(min_val, min(abs(val), max_val))
    
    298
    -        if value.endswith('pt'):  # points
    
    299
    -            size = get_val(5, 50)
    
    300
    -            if size is None:
    
    301
    -                return
    
    302
    -            callback(size, 'pt', *args)
    
    303
    -
    
    304
    -        elif value.endswith('em'):
    
    305
    -            size = get_val(0.3, 4)
    
    306
    -            if size is None:
    
    307
    -                return
    
    308
    -            callback(size, 'em', *args)
    
    309
    -
    
    310
    -        elif value.endswith('px'):  # pixels
    
    311
    -            size = get_val(5, 50)
    
    312
    -            if size is None:
    
    313
    -                return
    
    314
    -            callback(size, 'px', *args)
    
    315
    -
    
    316
    -        else:
    
    317
    -            try:
    
    318
    -                # TODO: isn't "no units" interpreted as pixels?
    
    319
    -                val = int(value)
    
    320
    -                if val > 0:
    
    321
    -                    sign = 1
    
    322
    -                elif val < 0:
    
    323
    -                    sign = -1
    
    324
    -                else:
    
    325
    -                    sign = 0
    
    326
    -                # validate length
    
    327
    -                val = sign*max(5, min(abs(val), 70))
    
    328
    -                callback(val, 'px', *args)
    
    329
    -            except Exception:
    
    330
    -                log.warning('Unable to parse length value "%s"', value)
    
    331
    -
    
    332
    -    def __parse_font_size_cb(self, size, type_, tag):
    
    333
    -        if type_ in ('em', '%'):
    
    334
    -            tag.set_property('scale', size)
    
    335
    -        elif type_ == 'pt':
    
    336
    -            tag.set_property('size-points', size)
    
    337
    -        elif type_ == 'px':
    
    338
    -            tag.set_property('size-points', self._get_points_from_pixels(size))
    
    339
    -
    
    340
    -    @staticmethod
    
    341
    -    def _parse_style_display(tag, value):
    
    342
    -        if value == 'none':
    
    343
    -            tag.set_property('invisible', 'true')
    
    344
    -        # FIXME: display: block, inline
    
    345
    -
    
    346
    -    def _parse_style_font_size(self, tag, value):
    
    347
    -        try:
    
    348
    -            scale = {
    
    349
    -                'xx-small': 0.5787037037037,
    
    350
    -                'x-small': 0.6444444444444,
    
    351
    -                'small': 0.8333333333333,
    
    352
    -                'medium': 1.0,
    
    353
    -                'large': 1.2,
    
    354
    -                'x-large': 1.4399999999999,
    
    355
    -                'xx-large': 1.728,
    
    356
    -            }[value]
    
    357
    -        except KeyError:
    
    358
    -            pass
    
    359
    -        else:
    
    360
    -            tag.set_property('scale', scale)
    
    361
    -            return
    
    362
    -        if value == 'smaller':
    
    363
    -            tag.set_property('scale', 0.8333333333333)
    
    364
    -            return
    
    365
    -        if value == 'larger':
    
    366
    -            tag.set_property('scale', 1.2)
    
    367
    -            return
    
    368
    -        # font relative (5 ~ 4pt, 110 ~ 72pt)
    
    369
    -        self._parse_length(
    
    370
    -            value, True, False, 5, 110, self.__parse_font_size_cb, tag)
    
    371
    -
    
    372
    -    @staticmethod
    
    373
    -    def _parse_style_font_style(tag, value):
    
    374
    -        try:
    
    375
    -            style = {
    
    376
    -                'normal': Pango.Style.NORMAL,
    
    377
    -                'italic': Pango.Style.ITALIC,
    
    378
    -                'oblique': Pango.Style.OBLIQUE,
    
    379
    -            }[value]
    
    380
    -        except KeyError:
    
    381
    -            log.warning('unknown font-style %s', value)
    
    382
    -        else:
    
    383
    -            tag.set_property('style', style)
    
    384
    -
    
    385
    -    def __frac_length_tag_cb(self, length, tag, propname):
    
    386
    -        styles = self._get_style_tags()
    
    387
    -        if styles:
    
    388
    -            length += styles[-1].get_property(propname)
    
    389
    -        tag.set_property(propname, length)
    
    390
    -
    
    391
    -    def _parse_style_margin_left(self, tag, value):
    
    392
    -        # block relative
    
    393
    -        self._parse_length(value, False, True, 1, 1000,
    
    394
    -                           self.__frac_length_tag_cb, tag, 'left-margin')
    
    395
    -
    
    396
    -    def _parse_style_margin_right(self, tag, value):
    
    397
    -        # block relative
    
    398
    -        self._parse_length(value, False, True, 1, 1000,
    
    399
    -                           self.__frac_length_tag_cb, tag, 'right-margin')
    
    400
    -
    
    401
    -    @staticmethod
    
    402
    -    def _parse_style_font_weight(tag, value):
    
    403
    -        # TODO: missing 'bolder' and 'lighter'
    
    404
    -        try:
    
    405
    -            weight = {
    
    406
    -                '100': Pango.Weight.ULTRALIGHT,
    
    407
    -                '200': Pango.Weight.ULTRALIGHT,
    
    408
    -                '300': Pango.Weight.LIGHT,
    
    409
    -                '400': Pango.Weight.NORMAL,
    
    410
    -                '500': Pango.Weight.NORMAL,
    
    411
    -                '600': Pango.Weight.BOLD,
    
    412
    -                '700': Pango.Weight.BOLD,
    
    413
    -                '800': Pango.Weight.ULTRABOLD,
    
    414
    -                '900': Pango.Weight.HEAVY,
    
    415
    -                'normal': Pango.Weight.NORMAL,
    
    416
    -                'bold': Pango.Weight.BOLD,
    
    417
    -            }[value]
    
    418
    -        except KeyError:
    
    419
    -            log.warning('unknown font-style %s', value)
    
    420
    -        else:
    
    421
    -            tag.set_property('weight', weight)
    
    422
    -
    
    423
    -    @staticmethod
    
    424
    -    def _parse_style_font_family(tag, value):
    
    425
    -        tag.set_property('family', value)
    
    426
    -
    
    427
    -    @staticmethod
    
    428
    -    def _parse_style_text_align(tag, value):
    
    429
    -        try:
    
    430
    -            align = {
    
    431
    -                'left': Gtk.Justification.LEFT,
    
    432
    -                'right': Gtk.Justification.RIGHT,
    
    433
    -                'center': Gtk.Justification.CENTER,
    
    434
    -                'justify': Gtk.Justification.FILL,
    
    435
    -            }[value]
    
    436
    -        except KeyError:
    
    437
    -            log.warning('Invalid text-align: %s requested', value)
    
    438
    -        else:
    
    439
    -            tag.set_property('justification', align)
    
    440
    -
    
    441
    -    @staticmethod
    
    442
    -    def _parse_style_text_decoration(tag, value):
    
    443
    -        values = value.split(' ')
    
    444
    -        if 'none' in values:
    
    445
    -            tag.set_property('underline', Pango.Underline.NONE)
    
    446
    -            tag.set_property('strikethrough', False)
    
    447
    -        if 'underline' in values:
    
    448
    -            tag.set_property('underline', Pango.Underline.SINGLE)
    
    449
    -        else:
    
    450
    -            tag.set_property('underline', Pango.Underline.NONE)
    
    451
    -        if 'line-through' in values:
    
    452
    -            tag.set_property('strikethrough', True)
    
    453
    -        else:
    
    454
    -            tag.set_property('strikethrough', False)
    
    455
    -        if 'blink' in values:
    
    456
    -            log.warning('text-decoration:blink not implemented')
    
    457
    -        if 'overline' in values:
    
    458
    -            log.warning('text-decoration:overline not implemented')
    
    459
    -
    
    460
    -    @staticmethod
    
    461
    -    def _parse_style_white_space(tag, value):
    
    462
    -        if value == 'pre':
    
    463
    -            tag.set_property('wrap_mode', Gtk.WrapMode.NONE)
    
    464
    -        elif value == 'normal':
    
    465
    -            tag.set_property('wrap_mode', Gtk.WrapMode.WORD)
    
    466
    -        elif value == 'nowrap':
    
    467
    -            tag.set_property('wrap_mode', Gtk.WrapMode.NONE)
    
    468
    -
    
    469
    -    @staticmethod
    
    470
    -    def __length_tag_cb(value, tag, propname):
    
    471
    -        try:
    
    472
    -            tag.set_property(propname, value)
    
    473
    -        except Exception:
    
    474
    -            log.warning('Error with prop: %s for tag: %s', propname, str(tag))
    
    475
    -
    
    476
    -    def _parse_style_width(self, tag, value):
    
    477
    -        if value == 'auto':
    
    478
    -            return
    
    479
    -        self._parse_length(value, False, False, 1, 1000,
    
    480
    -                           self.__length_tag_cb, tag, "width")
    
    481
    -    def _parse_style_height(self, tag, value):
    
    482
    -        if value == 'auto':
    
    483
    -            return
    
    484
    -        self._parse_length(value, False, False, 1, 1000,
    
    485
    -                           self.__length_tag_cb, tag, "height")
    
    486
    -
    
    487
    -    def _get_style_tags(self):
    
    488
    -        return [tag for tag in self.styles if tag is not None]
    
    489
    -
    
    490
    -    def _create_url(self, href, title, type_, id_):
    
    491
    -        '''Process a url tag.
    
    492
    -        '''
    
    493
    -        tag = self.textbuf.create_tag(id_)
    
    494
    -        if href and href[0] != '#':
    
    495
    -            tag.href = href
    
    496
    -            tag.type_ = type_ # to be used by the URL handler
    
    497
    -            tag.connect('event', self.textview.hyperlink_handler, 'url')
    
    498
    -            tag.set_property('foreground',
    
    499
    -                             app.css_config.get_value('.gajim-url',
    
    500
    -                                                      StyleAttr.COLOR))
    
    501
    -            tag.set_property('underline', Pango.Underline.SINGLE)
    
    502
    -            tag.is_anchor = True
    
    503
    -        if title:
    
    504
    -            tag.title = title
    
    505
    -        return tag
    
    506
    -
    
    507
    -    def _begin_span(self, style, tag=None, id_=None):
    
    508
    -        if style is None:
    
    509
    -            self.styles.append(tag)
    
    510
    -            return
    
    511
    -        if tag is None:
    
    512
    -            if id_:
    
    513
    -                tag = self.textbuf.create_tag(id_)
    
    514
    -            else:
    
    515
    -                tag = self.textbuf.create_tag() # we create anonymous tag
    
    516
    -        for attr, val in style_iter(style):
    
    517
    -            attr = attr.lower()
    
    518
    -            try:
    
    519
    -                getattr(self, self.__style_methods[attr])(tag, val)
    
    520
    -            except KeyError:
    
    521
    -                log.warning('Style attribute "%s" requested '
    
    522
    -                            'but not yet implemented', attr)
    
    523
    -        self.styles.append(tag)
    
    524
    -
    
    525
    -    def _end_span(self):
    
    526
    -        self.styles.pop()
    
    527
    -
    
    528
    -    def _jump_line(self):
    
    529
    -        self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
    
    530
    -        self.starting = True
    
    531
    -
    
    532
    -    def _insert_text(self, text, working_iter=None):
    
    533
    -        if working_iter is None:
    
    534
    -            working_iter = self.iter
    
    535
    -        if self.starting and text != '\n':
    
    536
    -            self.starting = (text[-1] == '\n')
    
    537
    -        tags = self._get_style_tags()
    
    538
    -        if tags:
    
    539
    -            self.textbuf.insert_with_tags(working_iter, text, *tags)
    
    540
    -        else:
    
    541
    -            self.textbuf.insert(working_iter, text)
    
    542
    -
    
    543
    -    def _starts_line(self):
    
    544
    -        return self.starting or self.iter.starts_line()
    
    545
    -
    
    546
    -    def _flush_text(self):
    
    547
    -        if not self.text:
    
    548
    -            return
    
    549
    -        text, self.text = self.text, ''
    
    550
    -        if not self.preserve:
    
    551
    -            text = text.replace('\n', ' ')
    
    552
    -            self.handle_specials(whitespace_rx.sub(' ', text))
    
    553
    -        else:
    
    554
    -            self._insert_text(text.strip('\n'))
    
    555
    -
    
    556
    -    def _anchor_event(self, _tag, _textview, event, _iter, href, type_):
    
    557
    -        if event.type == Gdk.EventType.BUTTON_PRESS:
    
    558
    -            self.textview.emit('url-clicked', href, type_)
    
    559
    -            return True
    
    560
    -        return False
    
    561
    -
    
    562
    -    def handle_specials(self, text):
    
    563
    -        # TODO: Parse formatting
    
    564
    -        # self.iter = self.textview.parse_formatting(
    
    565
    -        #    text, self._get_style_tags(), iter_=self.iter)
    
    566
    -        self._insert_text(text)
    
    567
    -
    
    568
    -    def characters(self, content):
    
    569
    -        if self.preserve:
    
    570
    -            self.text += content
    
    571
    -            return
    
    572
    -        if allwhitespace_rx.match(content) is not None and self._starts_line():
    
    573
    -            return
    
    574
    -        self.text += content
    
    575
    -        self.starting = False
    
    576
    -
    
    577
    -    def startElement(self, name, attrs):
    
    578
    -        self._flush_text()
    
    579
    -        klass = [i for i in attrs.get('class', ' ').split(' ') if i]
    
    580
    -        style = ''
    
    581
    -        #Add styles defined for classes
    
    582
    -        for k in klass:
    
    583
    -            if k  in classes:
    
    584
    -                style += classes[k]
    
    585
    -
    
    586
    -        tag = None
    
    587
    -        #FIXME: if we want to use id, it needs to be unique across
    
    588
    -        # the whole textview, so we need to add something like the
    
    589
    -        # message-id to it.
    
    590
    -        #id_ = attrs.get('id',None)
    
    591
    -        id_ = None
    
    592
    -        if name == 'a':
    
    593
    -            #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
    
    594
    -            href = attrs.get('href', None)
    
    595
    -            if not href:
    
    596
    -                href = attrs.get('HREF', None)
    
    597
    -            # Gaim sends HREF instead of href
    
    598
    -            title = attrs.get('title', attrs.get('rel', href))
    
    599
    -            type_ = attrs.get('type', None)
    
    600
    -            tag = self._create_url(href, title, type_, id_)
    
    601
    -        elif name == 'blockquote':
    
    602
    -            cite = attrs.get('cite', None)
    
    603
    -            if cite:
    
    604
    -                tag = self.textbuf.create_tag(id_)
    
    605
    -                tag.title = attrs.get('title', None)
    
    606
    -                tag.is_anchor = True
    
    607
    -        elif name in LIST_ELEMS:
    
    608
    -            style += ';margin-left: 2em'
    
    609
    -
    
    610
    -        if name in _element_styles:
    
    611
    -            style += _element_styles[name]
    
    612
    -        # so that explicit styles override implicit ones,
    
    613
    -        # we add the attribute last
    
    614
    -        style += ";"+attrs.get('style', '')
    
    615
    -        if style == '':
    
    616
    -            style = None
    
    617
    -        self._begin_span(style, tag, id_)
    
    618
    -
    
    619
    -        if name == 'br':
    
    620
    -            pass # handled in endElement
    
    621
    -        elif name == 'hr':
    
    622
    -            pass # handled in endElement
    
    623
    -        elif name in BLOCK:
    
    624
    -            if not self._starts_line():
    
    625
    -                self._jump_line()
    
    626
    -            if name == 'pre':
    
    627
    -                self.preserve = True
    
    628
    -        elif name == 'span':
    
    629
    -            pass
    
    630
    -        elif name in ('dl', 'ul'):
    
    631
    -            if not self._starts_line():
    
    632
    -                self._jump_line()
    
    633
    -            self.list_counters.append(None)
    
    634
    -        elif name == 'ol':
    
    635
    -            if not self._starts_line():
    
    636
    -                self._jump_line()
    
    637
    -            self.list_counters.append(0)
    
    638
    -        elif name == 'li':
    
    639
    -            if self.list_counters[-1] is None:
    
    640
    -                li_head = chr(0x2022)
    
    641
    -            else:
    
    642
    -                self.list_counters[-1] += 1
    
    643
    -                li_head = '%i.' % self.list_counters[-1]
    
    644
    -            self.text = ' '*len(self.list_counters)*4 + li_head + ' '
    
    645
    -            self._flush_text()
    
    646
    -            self.starting = True
    
    647
    -        elif name == 'dd':
    
    648
    -            self._jump_line()
    
    649
    -        elif name == 'dt':
    
    650
    -            if not self.starting:
    
    651
    -                self._jump_line()
    
    652
    -        elif name in ('a', 'img', 'body', 'html'):
    
    653
    -            pass
    
    654
    -        elif name in INLINE:
    
    655
    -            pass
    
    656
    -        else:
    
    657
    -            log.warning('Unhandled element "%s"', name)
    
    658
    -
    
    659
    -    def endElement(self, name):
    
    660
    -        end_preserving = False
    
    661
    -        newline = False
    
    662
    -        if name == 'br':
    
    663
    -            newline = True
    
    664
    -        elif name == 'hr':
    
    665
    -            #FIXME: plenty of unused attributes (width, height,...) :)
    
    666
    -            self._jump_line()
    
    667
    -            self._insert_text('\u2015'*40)
    
    668
    -            self._jump_line()
    
    669
    -        elif name in LIST_ELEMS:
    
    670
    -            self.list_counters.pop()
    
    671
    -        elif name == 'li':
    
    672
    -            newline = True
    
    673
    -        elif name == 'img':
    
    674
    -            pass
    
    675
    -        elif name in ('body', 'html'):
    
    676
    -            pass
    
    677
    -        elif name == 'a':
    
    678
    -            pass
    
    679
    -        elif name in INLINE:
    
    680
    -            pass
    
    681
    -        elif name in ('dd', 'dt', ):
    
    682
    -            pass
    
    683
    -        elif name in BLOCK:
    
    684
    -            if name == 'pre':
    
    685
    -                end_preserving = True
    
    686
    -            elif name in BLOCK_STRUCT:
    
    687
    -                newline = True
    
    688
    -        else:
    
    689
    -            log.warning("Unhandled element '%s'", name)
    
    690
    -        self._flush_text()
    
    691
    -        if end_preserving:
    
    692
    -            self.preserve = False
    
    693
    -        if newline:
    
    694
    -            self._jump_line()
    
    695
    -        self._end_span()
    
    696
    -
    
    697
    -
    
    698
    -class HtmlTextView(Gtk.TextView):
    
    699
    -
    
    700
    -    _tags = ['url', 'mail', 'xmpp', 'sth_at_sth']
    
    701
    -
    
    702
    -    def __init__(self, account, standalone=False):
    
    703
    -        Gtk.TextView.__init__(self)
    
    704
    -
    
    705
    -        self.set_has_tooltip(True)
    
    706
    -        self.set_border_width(1)
    
    707
    -        self.set_accepts_tab(True)
    
    708
    -        self.set_editable(False)
    
    709
    -        self.set_cursor_visible(False)
    
    710
    -        self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
    
    711
    -        self.set_left_margin(2)
    
    712
    -        self.set_right_margin(2)
    
    713
    -
    
    714
    -        self.drag_dest_unset()
    
    715
    -
    
    716
    -        self.connect('copy-clipboard', self._on_copy_clipboard)
    
    717
    -        self.connect('destroy', self._on_destroy)
    
    718
    -        self.get_buffer().eol_tag = self.get_buffer().create_tag('eol')
    
    719
    -
    
    720
    -        self.account = account
    
    721
    -        self.plugin_modified = False
    
    722
    -        self._cursor_changed = False
    
    723
    -
    
    724
    -        if standalone:
    
    725
    -            self.connect('query-tooltip', self._query_tooltip)
    
    726
    -            self.create_tags()
    
    727
    -
    
    728
    -    def _on_destroy(self, *args):
    
    729
    -        # We restore the TextView’s drag destination to avoid a GTK warning
    
    730
    -        # when closing the control. BaseControl.shutdown() calls destroy()
    
    731
    -        # on the control’s main box, causing GTK to recursively destroy the
    
    732
    -        # child widgets. GTK then tries to set a target list on the TextView,
    
    733
    -        # resulting in a warning because the Widget has no drag destination.
    
    734
    -        self.drag_dest_set(
    
    735
    -            Gtk.DestDefaults.ALL,
    
    736
    -            None,
    
    737
    -            Gdk.DragAction.DEFAULT)
    
    738
    -
    
    739
    -    def create_tags(self):
    
    740
    -        color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR)
    
    741
    -
    
    742
    -        attrs = {'foreground': color,
    
    743
    -                 'underline': Pango.Underline.SINGLE}
    
    744
    -
    
    745
    -        for tag in self._tags:
    
    746
    -            tag_ = self.get_buffer().create_tag(tag, **attrs)
    
    747
    -            tag_.connect('event', self.hyperlink_handler, tag)
    
    748
    -
    
    749
    -    def update_tags(self):
    
    750
    -        tag_table = self.get_buffer().get_tag_table()
    
    751
    -        color = app.css_config.get_value('.gajim-url', StyleAttr.COLOR)
    
    752
    -
    
    753
    -        for tag in self._tags:
    
    754
    -            tag_table.lookup(tag).set_property('foreground', color)
    
    755
    -
    
    756
    -    def _query_tooltip(self, widget, x_pos, y_pos, _keyboard_mode, tooltip):
    
    757
    -        window = widget.get_window(Gtk.TextWindowType.TEXT)
    
    758
    -        x_pos, y_pos = self.window_to_buffer_coords(
    
    759
    -            Gtk.TextWindowType.TEXT, x_pos, y_pos)
    
    760
    -        iter_ = self.get_iter_at_position(x_pos, y_pos)[1]
    
    761
    -
    
    762
    -        for tag in iter_.get_tags():
    
    763
    -            if getattr(tag, 'is_anchor', False):
    
    764
    -                text = getattr(tag, 'title', False)
    
    765
    -                if text:
    
    766
    -                    if len(text) > 50:
    
    767
    -                        text = text[:47] + '…'
    
    768
    -                    tooltip.set_text(text)
    
    769
    -                    window.set_cursor(get_cursor('pointer'))
    
    770
    -                    self._cursor_changed = True
    
    771
    -                    return True
    
    772
    -
    
    773
    -            tag_name = tag.get_property('name')
    
    774
    -            if tag_name in ('url', 'mail', 'xmpp', 'sth_at_sth'):
    
    775
    -                window.set_cursor(get_cursor('pointer'))
    
    776
    -                self._cursor_changed = True
    
    777
    -                return False
    
    778
    -
    
    779
    -        if self._cursor_changed:
    
    780
    -            window.set_cursor(get_cursor('text'))
    
    781
    -            self._cursor_changed = False
    
    782
    -        return False
    
    783
    -
    
    784
    -    def show_context_menu(self, uri):
    
    785
    -        menu = get_conv_context_menu(self.account, uri)
    
    786
    -        if menu is None:
    
    787
    -            log.warning('No handler for URI type: %s', uri)
    
    788
    -            return
    
    789
    -
    
    790
    -        def destroy(menu, _pspec):
    
    791
    -            visible = menu.get_property('visible')
    
    792
    -            if not visible:
    
    793
    -                GLib.idle_add(menu.destroy)
    
    794
    -
    
    795
    -        menu.attach_to_widget(self, None)
    
    796
    -        menu.connect('notify::visible', destroy)
    
    797
    -        menu.popup_at_pointer()
    
    798
    -
    
    799
    -    def hyperlink_handler(self, texttag, _widget, event, iter_, _kind):
    
    800
    -        if event.type != Gdk.EventType.BUTTON_PRESS:
    
    801
    -            return Gdk.EVENT_PROPAGATE
    
    802
    -
    
    803
    -        begin_iter = iter_.copy()
    
    804
    -        # we get the beginning of the tag
    
    805
    -        while not begin_iter.starts_tag(texttag):
    
    806
    -            begin_iter.backward_char()
    
    807
    -        end_iter = iter_.copy()
    
    808
    -        # we get the end of the tag
    
    809
    -        while not end_iter.ends_tag(texttag):
    
    810
    -            end_iter.forward_char()
    
    811
    -
    
    812
    -        # Detect XHTML-IM link
    
    813
    -        word = getattr(texttag, 'href', None)
    
    814
    -        if not word:
    
    815
    -            word = self.get_buffer().get_text(begin_iter, end_iter, True)
    
    816
    -
    
    817
    -        uri = parse_uri(word)
    
    818
    -        if event.button.button == 3: # right click
    
    819
    -            self.show_context_menu(uri)
    
    820
    -            return Gdk.EVENT_STOP
    
    821
    -
    
    822
    -        self.plugin_modified = False
    
    823
    -        app.plugin_manager.extension_point(
    
    824
    -            'hyperlink_handler', uri, self, self.get_toplevel())
    
    825
    -        if self.plugin_modified:
    
    826
    -            return Gdk.EVENT_STOP
    
    827
    -
    
    828
    -        open_uri(uri, account=self.account)
    
    829
    -        return Gdk.EVENT_STOP
    
    830
    -
    
    831
    -    def display_html(self, html, textview, iter_=None):
    
    832
    -        buffer_ = self.get_buffer()
    
    833
    -        if iter_:
    
    834
    -            eob = iter_
    
    835
    -        else:
    
    836
    -            eob = buffer_.get_end_iter()
    
    837
    -        parser = xml.sax.make_parser()
    
    838
    -        parser.setContentHandler(HtmlHandler(textview, eob))
    
    839
    -        parser.parse(StringIO(html))
    
    840
    -        # If the xhtml ends with a BLOCK element we have to remove
    
    841
    -        # the \n we add after BLOCK elements
    
    842
    -        self._delete_last_char(buffer_, eob)
    
    843
    -
    
    844
    -    @staticmethod
    
    845
    -    def _delete_last_char(buffer_, iter_):
    
    846
    -        start_iter = iter_.copy()
    
    847
    -        start_iter.backward_char()
    
    848
    -        text = buffer_.get_text(start_iter, iter_, True)
    
    849
    -        if text == '\n':
    
    850
    -            buffer_.delete(start_iter, iter_)
    
    851
    -
    
    852
    -    @staticmethod
    
    853
    -    def _on_copy_clipboard(textview):
    
    854
    -        clipboard = textview.get_clipboard(Gdk.SELECTION_CLIPBOARD)
    
    855
    -        selected = textview.get_selected_text()
    
    856
    -        clipboard.set_text(selected, -1)
    
    857
    -        GObject.signal_stop_emission_by_name(textview, 'copy-clipboard')
    
    858
    -
    
    859
    -    def get_selected_text(self):
    
    860
    -        bounds = self.get_buffer().get_selection_bounds()
    
    861
    -        selection = ''
    
    862
    -        if bounds:
    
    863
    -            (search_iter, end) = bounds
    
    864
    -
    
    865
    -            while search_iter.compare(end):
    
    866
    -                character = search_iter.get_char()
    
    867
    -                if character == '\ufffc':
    
    868
    -                    anchor = search_iter.get_child_anchor()
    
    869
    -                    if anchor:
    
    870
    -                        text = anchor.plaintext
    
    871
    -                        if text:
    
    872
    -                            selection += text
    
    873
    -                    else:
    
    874
    -                        selection += character
    
    875
    -                else:
    
    876
    -                    selection += character
    
    877
    -                search_iter.forward_char()
    
    878
    -        return selection
    
    879
    -
    
    880
    -    def replace_emojis(self, start_mark, end_mark, pixbuf, codepoint):
    
    881
    -        buffer_ = self.get_buffer()
    
    882
    -        start_iter = buffer_.get_iter_at_mark(start_mark)
    
    883
    -        end_iter = buffer_.get_iter_at_mark(end_mark)
    
    884
    -        buffer_.delete(start_iter, end_iter)
    
    885
    -
    
    886
    -        anchor = buffer_.create_child_anchor(start_iter)
    
    887
    -        anchor.plaintext = codepoint
    
    888
    -        emoji = Gtk.Image.new_from_pixbuf(pixbuf)
    
    889
    -        emoji.show()
    
    890
    -        self.add_child_at_anchor(emoji, anchor)
    
    891
    -        buffer_.delete_mark(start_mark)
    
    892
    -        buffer_.delete_mark(end_mark)
    
    893
    -
    
    894
    -    def unselect(self):
    
    895
    -        buffer_ = self.get_buffer()
    
    896
    -        insert_iter = buffer_.get_iter_at_mark(buffer_.get_insert())
    
    897
    -        buffer_.select_range(insert_iter, insert_iter)