~ubuntu-branches/ubuntu/oneiric/emesene/oneiric-proposed

« back to all changes in this revision

Viewing changes to htmltextview.py

  • Committer: Bazaar Package Importer
  • Author(s): Devid Antonio Filoni
  • Date: 2011-03-03 14:49:13 UTC
  • mfrom: (1.1.9 upstream)
  • Revision ID: james.westby@ubuntu.com-20110303144913-0adl9cmw2s35lvzo
Tags: 2.0~git20110303-0ubuntu1
* New upstream git revision (LP: #728469).
* Remove debian/watch, debian/emesene.xpm, debian/install and
  debian/README.source files.
* Remove 21_svn2451_fix_avatar and 20_dont_build_own_libmimic patches.
* debian/control: modify python to python (>= 2.5) in Build-Depends field.
* debian/control: remove python-libmimic from Recommends field.
* debian/control: modify python-gtk2 (>= 2.10) to python-gtk2 (>= 2.12) in
  Depends field.
* debian/control: add python-appindicator and python-xmpp to Recommends
  field.
* debian/control: add python-papyon (>= 0.5.4) and python-webkit to Depends
  field.
* debian/control: update Description field.
* debian/control: add python-setuptools to Build-Depends field.
* debian/control: move python-dbus and python-notify to Depends field.
* Update debian/copyright file.
* Update debian/links file.
* debian/menu: update description field.
* Bump Standards-Version to 3.9.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
 
3
 
### Copyright (C) 2005 Gustavo J. A. M. Carneiro
4
 
###
5
 
### This library is free software; you can redistribute it and/or
6
 
### modify it under the terms of the GNU Lesser General Public
7
 
### License as published by the Free Software Foundation; either
8
 
### version 2.1 of the License, or (at your option) any later version.
9
 
###
10
 
### This library is distributed in the hope that it will be useful,
11
 
### but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13
 
### Lesser General Public License for more details.
14
 
###
15
 
### You should have received a copy of the GNU Lesser General Public
16
 
### License along with this library; if not, write to the Free Software
17
 
### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
18
 
 
19
 
'''
20
 
A gtk.TextView-based renderer for XHTML-IM, as described in:
21
 
  http://www.jabber.org/jeps/jep-0071.html
22
 
'''
23
 
import gobject
24
 
import pango
25
 
import gtk
26
 
import xml.sax, xml.sax.handler
27
 
import re
28
 
import warnings
29
 
import urllib
30
 
from cStringIO import StringIO
31
 
import operator
32
 
import desktop
33
 
 
34
 
import stock
35
 
import dialog
36
 
 
37
 
__all__ = ['HtmlTextView']
38
 
 
39
 
whitespace_rx = re.compile("\\s+")
40
 
 
41
 
if gtk.gdk.screen_height_mm():
42
 
    ## pixels = points * display_resolution
43
 
    display_resolution = 0.3514598*(gtk.gdk.screen_height() /
44
 
                        float(gtk.gdk.screen_height_mm()))
45
 
else:
46
 
    # inaccurate, but better than letting the universe implode
47
 
    display_resolution = 1
48
 
 
49
 
 
50
 
def _parse_css_color(color):
51
 
    '''_parse_css_color(css_color) -> gtk.gdk.Color'''
52
 
    if color.startswith("rgb(") and color.endswith(')'):
53
 
        r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
54
 
        return gtk.gdk.Color(r, g, b)
55
 
    else: # there's some strange issue with colors starting with double #
56
 
        if color.startswith('##'): 
57
 
            color = '#0' + color.lstrip('#')
58
 
        return gtk.gdk.color_parse(color)
59
 
 
60
 
 
61
 
class HtmlHandler:
62
 
 
63
 
    def __init__(self, textview, startiter, canResize):
64
 
        self.textbuf = textview.get_buffer()
65
 
        self.textview = textview
66
 
        self.iter = startiter
67
 
        self.text = ''
68
 
        self.styles = [] # a gtk.TextTag or None, for each span level
69
 
        self.list_counters = [] # stack (top at head) of list
70
 
                                # counters, or None for unordered list
71
 
        self.canResize = canResize
72
 
        
73
 
    def _parse_style_color(self, tag, value):
74
 
        color = _parse_css_color(value)
75
 
        tag.set_property("foreground-gdk", color)
76
 
 
77
 
    def _parse_style_background_color(self, tag, value):
78
 
        color = _parse_css_color(value)
79
 
        tag.set_property("background-gdk", color)
80
 
        if gtk.gtk_version >= (2, 8):
81
 
            tag.set_property("paragraph-background-gdk", color)
82
 
 
83
 
 
84
 
    if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1):
85
 
 
86
 
        def _get_current_attributes(self):
87
 
            attrs = self.textview.get_default_attributes()
88
 
            self.iter.backward_char()
89
 
            self.iter.get_attributes(attrs)
90
 
            self.iter.forward_char()
91
 
            return attrs
92
 
 
93
 
    else:
94
 
 
95
 
        ## Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455
96
 
        def _get_current_style_attr(self, propname, comb_oper=None):
97
 
            tags = [tag for tag in self.styles if tag is not None]
98
 
            tags.reverse()
99
 
            is_set_name = propname + "-set"
100
 
            value = None
101
 
            for tag in tags:
102
 
                if tag.get_property(is_set_name):
103
 
                    if value is None:
104
 
                        value = tag.get_property(propname)
105
 
                        if comb_oper is None:
106
 
                            return value
107
 
                    else:
108
 
                        value = comb_oper(value, tag.get_property(propname))
109
 
            return value
110
 
 
111
 
        class _FakeAttrs(object):
112
 
            __slots__ = ("font", "font_scale")
113
 
 
114
 
        def _get_current_attributes(self):
115
 
            attrs = self._FakeAttrs()
116
 
            attrs.font_scale = self._get_current_style_attr("scale",
117
 
                                                            operator.mul)
118
 
            if attrs.font_scale is None:
119
 
                attrs.font_scale = 1.0
120
 
            attrs.font = self._get_current_style_attr("font-desc")
121
 
            if attrs.font is None:
122
 
                attrs.font = self.textview.style.font_desc
123
 
            return attrs
124
 
 
125
 
 
126
 
    def __parse_length_frac_size_allocate(self, textview, allocation,
127
 
                                          frac, callback, args):
128
 
        callback(allocation.width*frac, *args)
129
 
 
130
 
    def _parse_length(self, value, font_relative, callback, *args):
131
 
        '''Parse/calc length, converting to pixels, calls callback(length, *args)
132
 
        when the length is first computed or changes'''
133
 
        if value.endswith('%'):
134
 
            frac = float(value[:-1])/100
135
 
            if font_relative:
136
 
                attrs = self._get_current_attributes()
137
 
                font_size = attrs.font.get_size() / pango.SCALE
138
 
                callback(frac*display_resolution*font_size, *args)
139
 
            else:
140
 
                ## CSS says "Percentage values: refer to width of the closest
141
 
                ##           block-level ancestor"
142
 
                ## This is difficult/impossible to implement, so we use
143
 
                ## textview width instead; a reasonable approximation..
144
 
                alloc = self.textview.get_allocation()
145
 
                self.__parse_length_frac_size_allocate(self.textview, alloc,
146
 
                                                       frac, callback, args)
147
 
                self.textview.connect("size-allocate",
148
 
                                      self.__parse_length_frac_size_allocate,
149
 
                                      frac, callback, args)
150
 
 
151
 
        elif value.endswith('pt'): # points
152
 
            callback(float(value[:-2])*display_resolution, *args)
153
 
 
154
 
        elif value.endswith('em'): # ems, the height of the element's font
155
 
            attrs = self._get_current_attributes()
156
 
            font_size = attrs.font.get_size() / pango.SCALE
157
 
            callback(float(value[:-2])*display_resolution*font_size, *args)
158
 
 
159
 
        elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
160
 
            ## FIXME: figure out how to calculate this correctly
161
 
            ##        for now 'em' size is used as approximation
162
 
            attrs = self._get_current_attributes()
163
 
            font_size = attrs.font.get_size() / pango.SCALE
164
 
            callback(float(value[:-2])*display_resolution*font_size, *args)
165
 
 
166
 
        elif value.endswith('px'): # pixels
167
 
            callback(int(value[:-2]), *args)
168
 
 
169
 
        else:
170
 
            warnings.warn("Unable to parse length value '%s'" % value)
171
 
 
172
 
    def __parse_font_size_cb(length, tag):
173
 
        tag.set_property("size-points", length/display_resolution)
174
 
    __parse_font_size_cb = staticmethod(__parse_font_size_cb)
175
 
 
176
 
    def _parse_style_font_size(self, tag, value):
177
 
        try:
178
 
            scale = {
179
 
                "xx-small": pango.SCALE_XX_SMALL,
180
 
                "x-small": pango.SCALE_X_SMALL,
181
 
                "small": pango.SCALE_SMALL,
182
 
                "medium": pango.SCALE_MEDIUM,
183
 
                "large": pango.SCALE_LARGE,
184
 
                "x-large": pango.SCALE_X_LARGE,
185
 
                "xx-large": pango.SCALE_XX_LARGE,
186
 
                } [value]
187
 
        except KeyError:
188
 
            pass
189
 
        else:
190
 
            attrs = self._get_current_attributes()
191
 
            tag.set_property("scale", scale / attrs.font_scale)
192
 
            return
193
 
        if value == 'smaller':
194
 
            tag.set_property("scale", pango.SCALE_SMALL)
195
 
            return
196
 
        if value == 'larger':
197
 
            tag.set_property("scale", pango.SCALE_LARGE)
198
 
            return
199
 
        self._parse_length(value, True, self.__parse_font_size_cb, tag)
200
 
 
201
 
    def _parse_style_font_style(self, tag, value):
202
 
        try:
203
 
            style = {
204
 
                "normal": pango.STYLE_NORMAL,
205
 
                "italic": pango.STYLE_ITALIC,
206
 
                "oblique": pango.STYLE_OBLIQUE,
207
 
                } [value]
208
 
        except KeyError:
209
 
            warnings.warn("unknown font-style %s" % value)
210
 
        else:
211
 
            tag.set_property("style", style)
212
 
 
213
 
    def __frac_length_tag_cb(length, tag, propname):
214
 
        tag.set_property(propname, length)
215
 
    __frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
216
 
 
217
 
    def _parse_style_margin_left(self, tag, value):
218
 
        self._parse_length(value, False, self.__frac_length_tag_cb,
219
 
                           tag, "left-margin")
220
 
 
221
 
    def _parse_style_margin_right(self, tag, value):
222
 
        self._parse_length(value, False, self.__frac_length_tag_cb,
223
 
                           tag, "right-margin")
224
 
 
225
 
    def _parse_style_font_weight(self, tag, value):
226
 
        ## TODO: missing 'bolder' and 'lighter'
227
 
        try:
228
 
            weight = {
229
 
                '100': pango.WEIGHT_ULTRALIGHT,
230
 
                '200': pango.WEIGHT_ULTRALIGHT,
231
 
                '300': pango.WEIGHT_LIGHT,
232
 
                '400': pango.WEIGHT_NORMAL,
233
 
                '500': pango.WEIGHT_NORMAL,
234
 
                '600': pango.WEIGHT_BOLD,
235
 
                '700': pango.WEIGHT_BOLD,
236
 
                '800': pango.WEIGHT_ULTRABOLD,
237
 
                '900': pango.WEIGHT_HEAVY,
238
 
                'normal': pango.WEIGHT_NORMAL,
239
 
                'bold': pango.WEIGHT_BOLD,
240
 
                } [value]
241
 
        except KeyError:
242
 
            warnings.warn("unknown font-style %s" % value)
243
 
        else:
244
 
            tag.set_property("weight", weight)
245
 
 
246
 
    def _parse_style_font_family(self, tag, value):
247
 
        tag.set_property("family", value)
248
 
 
249
 
    def _parse_style_text_align(self, tag, value):
250
 
        try:
251
 
            align = {
252
 
                'left': gtk.JUSTIFY_LEFT,
253
 
                'right': gtk.JUSTIFY_RIGHT,
254
 
                'center': gtk.JUSTIFY_CENTER,
255
 
                'justify': gtk.JUSTIFY_FILL,
256
 
                } [value]
257
 
        except KeyError:
258
 
            warnings.warn("Invalid text-align:%s requested" % value)
259
 
        else:
260
 
            tag.set_property("justification", align)
261
 
 
262
 
    def _parse_style_text_decoration(self, tag, value):
263
 
        tag.set_property("strikethrough", False)
264
 
        tag.set_property("underline", pango.UNDERLINE_NONE)
265
 
 
266
 
        for chunk in value.split(' '):
267
 
            if chunk == "underline":
268
 
                tag.set_property("underline", pango.UNDERLINE_SINGLE)
269
 
            elif chunk == "overline":
270
 
                warnings.warn("text-decoration:overline not implemented")
271
 
            elif chunk == "line-through":
272
 
                tag.set_property("strikethrough", True)
273
 
            elif value == "blink":
274
 
                warnings.warn("text-decoration:blink not implemented")
275
 
            else:
276
 
                warnings.warn("text-decoration:%s not implemented" % value)
277
 
 
278
 
 
279
 
    ## build a dictionary mapping styles to methods, for greater speed
280
 
    __style_methods = dict()
281
 
    for style in ["background-color", "color", "font-family", "font-size",
282
 
                  "font-style", "font-weight", "margin-left", "margin-right",
283
 
                  "text-align", "text-decoration"]:
284
 
        try:
285
 
            method = locals()["_parse_style_%s" % style.replace('-', '_')]
286
 
        except KeyError:
287
 
            warnings.warn("Style attribute '%s' not yet implemented" % style)
288
 
        else:
289
 
            __style_methods[style] = method
290
 
    del style
291
 
    ## --
292
 
 
293
 
    def _get_style_tags(self):
294
 
        return [tag for tag in self.styles if tag is not None]
295
 
 
296
 
 
297
 
    def _begin_span(self, style, tag=None):
298
 
        if style is None:
299
 
            self.styles.append(tag)
300
 
            return None
301
 
        if tag is None:
302
 
            tag = self.textbuf.create_tag()
303
 
 
304
 
        l = [item.split(':', 1) for item in style.split(';')]
305
 
        l = [ x for x in l if len( x ) == 2 ]
306
 
        for attr, val in l:
307
 
            attr = attr.strip().lower()
308
 
            # strip brake the font-family value
309
 
            # for example Sans Serif -> SansSerif
310
 
            val = val.lstrip().rstrip()
311
 
            try:
312
 
                method = self.__style_methods[attr]
313
 
            except KeyError:
314
 
                warnings.warn("Style attribute '%s' requested "
315
 
                              "but not yet implemented" % attr)
316
 
            else:
317
 
                method(self, tag, val)
318
 
 
319
 
        self.styles.append(tag)
320
 
 
321
 
    def _end_span(self):
322
 
        self.styles.pop(-1)
323
 
 
324
 
    def _insert_text(self, text):
325
 
        tags = self._get_style_tags()
326
 
        if tags:
327
 
            self.textbuf.insert_with_tags(self.iter, text, *tags)
328
 
        else:
329
 
            self.textbuf.insert(self.iter, text)
330
 
 
331
 
    def _flush_text(self):
332
 
        if not self.text: return
333
 
        self._insert_text(self.text.replace('\n', ''))
334
 
        self.text = ''
335
 
 
336
 
    def anchor_menu_copy_link(self, widget, href):
337
 
        clip = gtk.clipboard_get()
338
 
        clip.set_text(href)
339
 
 
340
 
    def anchor_menu_open_link(self, widget, href, type_):
341
 
        self.textview.emit("url-clicked", href, type_)
342
 
 
343
 
    def make_anchor_context_menu(self, event, href, type_):
344
 
        menu = gtk.Menu()
345
 
 
346
 
        menu_items = gtk.ImageMenuItem( _( "_Open link" ) )
347
 
        menu_items.set_image( gtk.image_new_from_stock( gtk.STOCK_NETWORK, gtk.ICON_SIZE_MENU ))
348
 
        menu.append(menu_items)
349
 
        menu_items.connect("activate", self.anchor_menu_open_link, href, type_)
350
 
        menu_items.show()
351
 
 
352
 
        menu_items = gtk.ImageMenuItem( _( "_Copy link location" ))
353
 
        menu_items.set_image( gtk.image_new_from_stock( gtk.STOCK_COPY, gtk.ICON_SIZE_MENU ))
354
 
        menu.append(menu_items)
355
 
        menu_items.connect("activate", self.anchor_menu_copy_link, href)
356
 
        menu_items.show()
357
 
 
358
 
        menu.popup(None, None, None, event.button, event.time)
359
 
 
360
 
    def _anchor_event(self, tag, textview, event, iter, href, type_):
361
 
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1:
362
 
            self.textview.emit("url-clicked", href, type_)
363
 
            return True
364
 
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
365
 
            self.make_anchor_context_menu( event, href, type_)
366
 
            return True
367
 
        return False
368
 
 
369
 
    def _object_event( self, widget, event, attrs ):
370
 
        callbacks = self.textview.customObjectsCallbacks
371
 
        if attrs['type'] in callbacks and callbacks[attrs['type']][1]:
372
 
            callbacks[attrs['type']][1]( widget, event, attrs )
373
 
 
374
 
    def char_data(self, content):
375
 
        self.text += content
376
 
 
377
 
    def start_element(self, name, attrs):
378
 
        self._flush_text()
379
 
        try:
380
 
            style = attrs['style']
381
 
        except KeyError:
382
 
            style = None
383
 
 
384
 
        tag = None
385
 
        if name == 'a':
386
 
            tag = self.textbuf.create_tag()
387
 
            tag.set_property('foreground', self.textview.linkColor)
388
 
            tag.set_property('underline', pango.UNDERLINE_SINGLE)
389
 
            try:
390
 
                type_ = attrs['type']
391
 
            except KeyError:
392
 
                type_ = None
393
 
            tag.connect('event', self._anchor_event, attrs['href'], type_)
394
 
            tag.is_anchor = True
395
 
 
396
 
        self._begin_span(style, tag)
397
 
 
398
 
        if name == 'br':
399
 
            pass # handled in endElement
400
 
        elif name == 'p':
401
 
            if not self.iter.starts_line():
402
 
                self._insert_text("\n")
403
 
        elif name == 'div':
404
 
            if not self.iter.starts_line():
405
 
                self._insert_text("\n")
406
 
        elif name == 'span':
407
 
            pass
408
 
        elif name == 'ul':
409
 
            if not self.iter.starts_line():
410
 
                self._insert_text("\n")
411
 
            self.list_counters.insert(0, None)
412
 
        elif name == 'ol':
413
 
            if not self.iter.starts_line():
414
 
                self._insert_text("\n")
415
 
            self.list_counters.insert(0, 0)
416
 
        elif name == 'li':
417
 
            if self.list_counters[0] is None:
418
 
                li_head = unichr(0x2022)
419
 
            else:
420
 
                self.list_counters[0] += 1
421
 
                li_head = "%i." % self.list_counters[0]
422
 
            self.text = ' '*len(self.list_counters)*4 + li_head + ' '
423
 
        elif name == 'img':
424
 
            try:
425
 
                ## Max image size = 10 MB (to try to prevent DoS)
426
 
                mem = urllib.urlopen(attrs['src']).read(10*1024*1024)
427
 
                ## Caveat: GdkPixbuf is known not to be safe to load
428
 
                ## images from network... this program is now potentially
429
 
                ## hackable ;)
430
 
                loader = gtk.gdk.PixbufLoader()
431
 
                loader.write(mem); loader.close()
432
 
 
433
 
                anchor = self.textbuf.create_child_anchor(self.iter)
434
 
                img = AnimatedResizableImage(100, 100, self.canResize)
435
 
                img.set_tooltip(attrs['alt'])
436
 
                img.set_from_custom_animation(loader.get_animation())
437
 
                img.show()
438
 
                self.textview.add_child_at_anchor(img, anchor)
439
 
                self.textview.scrollLater()
440
 
                self.textview.emotes[anchor] = img
441
 
            
442
 
            except Exception, ex:
443
 
                pixbuf = None
444
 
                try:
445
 
                    alt = attrs['alt']
446
 
                except KeyError:
447
 
                    alt = "Broken image"
448
 
 
449
 
        elif name == 'object':
450
 
            anchor = self.textbuf.create_child_anchor(self.iter)
451
 
            customObj = self.textview.setCustomObject( attrs['class'], \
452
 
                    type=attrs['type'], alternate_text=str(attrs['data']))
453
 
            
454
 
            event_box = gtk.EventBox()
455
 
            event_box.add( customObj )
456
 
            event_box.connect( 'event', self._object_event, attrs.copy() )
457
 
            event_box.set_visible_window( False )
458
 
            event_box.set_above_child( True )
459
 
            event_box.set_events(gtk.gdk.ALL_EVENTS_MASK)
460
 
            event_box.show_all()
461
 
            self.textview.add_child_at_anchor(event_box, anchor)
462
 
 
463
 
 
464
 
        elif name == 'body':
465
 
            pass
466
 
        elif name == 'a':
467
 
            pass
468
 
        else:
469
 
            warnings.warn("Unhandled element '%s'" % name)
470
 
 
471
 
    def end_element(self, name):
472
 
        self._flush_text()
473
 
        if name == 'p':
474
 
            if not self.iter.starts_line():
475
 
                self._insert_text("\n")
476
 
        elif name == 'div':
477
 
            if not self.iter.starts_line():
478
 
                self._insert_text("\n")
479
 
        elif name == 'span':
480
 
            pass
481
 
        elif name == 'br':
482
 
            self._insert_text("\n")
483
 
        elif name == 'ul':
484
 
            self.list_counters.pop()
485
 
        elif name == 'ol':
486
 
            self.list_counters.pop()
487
 
        elif name == 'li':
488
 
            self._insert_text("\n")
489
 
        elif name == 'img':
490
 
            pass
491
 
        elif name == 'object':
492
 
            pass
493
 
        elif name == 'body':
494
 
            pass
495
 
        elif name == 'a':
496
 
            pass
497
 
        else:
498
 
            warnings.warn("Unhandled element '%s'" % name)
499
 
        self._end_span()
500
 
 
501
 
class AnimatedResizableImage(gtk.Image):
502
 
    def __init__(self, maxh=None, maxw=None, canResize=False):
503
 
        gtk.Image.__init__(self)    
504
 
        
505
 
        self.maxh = maxh
506
 
        self.maxw = maxw
507
 
        self.canResize = canResize
508
 
        self.edited_dimensions = False
509
 
        self.mustKill = False
510
 
        self.connect('destroy', self._must_destroy)
511
 
        
512
 
    def _must_destroy(self, *args):
513
 
        self.mustKill = True
514
 
        
515
 
    def set_tooltip(self, tooltiptext):
516
 
        self.set_property('has-tooltip', True)
517
 
        self.set_property('tooltip-text', tooltiptext)
518
 
 
519
 
    def set_from_filename(self, filename):
520
 
        animation = gtk.gdk.PixbufAnimation(filename)
521
 
        if animation.is_static_image():
522
 
            self._place_static(animation)
523
 
            return
524
 
        self._start_animation(animation)
525
 
        
526
 
    def set_from_custom_animation(self, animation):
527
 
        if animation.is_static_image():
528
 
            self._place_static(animation)
529
 
            return
530
 
        self._start_animation(animation)
531
 
          
532
 
    def _place_static(self, animation):
533
 
        self.h = animation.get_height()
534
 
        self.w = animation.get_width()
535
 
        ratio = float(self.w) / self.h
536
 
        while self.h > self.maxh or self.w > self.maxw:
537
 
            self.edited_dimensions = True
538
 
            if self.h < self.maxh:
539
 
                self.h -= 10
540
 
            else:
541
 
                self.h = self.maxh
542
 
            self.w = self.h * ratio
543
 
            
544
 
        if self.edited_dimensions and self.canResize:
545
 
            self.set_from_pixbuf(animation.get_static_image().scale_simple(\
546
 
                    int(self.w), self.h, gtk.gdk.INTERP_BILINEAR))    
547
 
        else:
548
 
            self.set_from_pixbuf(animation.get_static_image())    
549
 
            
550
 
    def _start_animation(self, animation):
551
 
        iteran = animation.get_iter()
552
 
        
553
 
        self.h = animation.get_height()
554
 
        self.w = animation.get_width()
555
 
        ratio = float(self.w) / self.h
556
 
        while self.h > self.maxh or self.w > self.maxw:
557
 
            self.edited_dimensions = True
558
 
            if self.h <= self.maxh:
559
 
                self.h -= 10
560
 
            else:
561
 
                self.h = self.maxh
562
 
            self.w = self.h * ratio  
563
 
        if not self.edited_dimensions or not self.canResize:
564
 
            self.set_from_animation(animation) # fallback to default handler
565
 
            return
566
 
         
567
 
        gobject.timeout_add(iteran.get_delay_time(), self._advance, iteran)
568
 
    
569
 
    def _advance(self, iteran):
570
 
        iteran.advance()
571
 
        if not self.mustKill:
572
 
            self.set_from_pixbuf(iteran.get_pixbuf().scale_simple(int(self.w), \
573
 
                    self.h, gtk.gdk.INTERP_NEAREST))
574
 
        else:
575
 
            return False # end the update game.
576
 
        
577
 
        gobject.timeout_add(iteran.get_delay_time(), self._advance, iteran)
578
 
        return False
579
 
  
580
 
 
581
 
class CustomEmoticonObject(object):
582
 
    def __init__(self, textview):
583
 
        self.imgs = []
584
 
        self.textview = textview
585
 
        self.pixbuf = None
586
 
        self.path = None
587
 
        
588
 
    def setNewImg(self, path=None, alttext=None, canResize = False):
589
 
        img = AnimatedResizableImage(100, 100, canResize)
590
 
        img.set_tooltip(alttext)
591
 
        img.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_SMALL_TOOLBAR)
592
 
        img.show()
593
 
        self.imgs.append(img)
594
 
 
595
 
        if path:
596
 
            self.setImgPath(path)
597
 
 
598
 
        self.updateImgs()
599
 
        return img
600
 
        
601
 
    def setImgPath( self, path ):
602
 
        self.path = path
603
 
        try:
604
 
            self.pixbuf = gtk.gdk.PixbufAnimation(path)
605
 
        except:
606
 
            pass
607
 
 
608
 
    def updateImgs( self ):
609
 
        if self.pixbuf is not None:
610
 
            self.textview.scrollLater()
611
 
            for img in self.imgs:
612
 
                img.set_from_custom_animation(self.pixbuf)
613
 
 
614
 
    def getImgs( self ):
615
 
        return self.imgs
616
 
 
617
 
class WinkObject(object):
618
 
    def __init__(self, textview):
619
 
        self.imgs = []
620
 
        self.textview = textview
621
 
        self.pixbuf = None
622
 
        self.path = None
623
 
 
624
 
    def setNewImg(self, path=None):
625
 
        img = gtk.Image()
626
 
        img.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_SMALL_TOOLBAR)
627
 
        img.show()
628
 
        self.imgs.append(img)
629
 
 
630
 
        if path:
631
 
            self.setImgPath(path)
632
 
 
633
 
        self.updateImgs()
634
 
        return img
635
 
 
636
 
    def setImgPath( self, path ):
637
 
        self.path = path
638
 
        try:
639
 
            self.pixbuf = gtk.gdk.pixbuf_new_from_file( path )
640
 
        except:
641
 
            pass
642
 
 
643
 
    def updateImgs( self ):
644
 
        if self.pixbuf is not None:
645
 
            self.textview.scrollLater()
646
 
            for img in self.imgs:
647
 
                img.set_from_pixbuf(self.pixbuf)
648
 
 
649
 
    def getImgs( self ):
650
 
        return self.imgs
651
 
 
652
 
 
653
 
class HtmlTextView(gtk.TextView):
654
 
    __gtype_name__ = 'HtmlTextView'
655
 
    __gsignals__ = {
656
 
        'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
657
 
    }
658
 
 
659
 
    def __init__(self, controller=None, buff=None, scrolledwin=None ):
660
 
        gtk.TextView.__init__(self, buff)
661
 
        self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
662
 
        self.set_editable(False)
663
 
        self.set_pixels_above_lines(2)
664
 
        self.set_pixels_below_lines(2)
665
 
 
666
 
        self._last_mark = None
667
 
        self._changed_cursor = False
668
 
        self.linkColor = "#0000FF"
669
 
 
670
 
        self.connect_after('copy-clipboard', self._on_copy_clipboard)
671
 
        self.emotes = {}
672
 
 
673
 
        self.connect("motion-notify-event", self.__motion_notify_event)
674
 
        self.connect("leave-notify-event", self.__leave_event)
675
 
        self.connect("enter-notify-event", self.__motion_notify_event)
676
 
        self.connect("url-clicked", self.on_link_clicked)
677
 
        if scrolledwin:
678
 
            scrolledwin.connect("expose-event", lambda x, y: self.queue_draw())
679
 
 
680
 
        self.controller = controller
681
 
        self.canResize = controller.config.user['canResizeEmoticons']
682
 
        
683
 
        self.customObjects = {}
684
 
        self.customObjectsCallbacks = {
685
 
            'application/x-emesene-emoticon': (self.customEmoticon,
686
 
                                                     self.customEmoticonEvent),
687
 
            'application/x-emesene-ink': (self.ink, None),
688
 
            'application/x-emesene-wink': (self.wink, None),
689
 
        }
690
 
    
691
 
    def customEmoticon(self, textview, id, path=None, alttext=None):
692
 
        if id in self.customObjects:
693
 
            return self.customObjects[id].setNewImg(path, alttext, self.canResize)
694
 
        else:
695
 
            customEmoticon = CustomEmoticonObject(textview)
696
 
            self.customObjects.update({id: customEmoticon})
697
 
            return customEmoticon.setNewImg(path, alttext, self.canResize)
698
 
 
699
 
    def wink(self, textview, id, path=None, alttext=None):
700
 
        if id in self.customObjects:
701
 
            return self.customObjects[id].setNewImg(path)
702
 
        else:
703
 
            wink = WinkObject(textview)
704
 
            self.customObjects.update({id: wink})
705
 
            return wink.setNewImg(path)
706
 
 
707
 
    def customEmoticonEvent(self, img, event, attrs):
708
 
        if not attrs['class'] in self.customObjects:
709
 
            return
710
 
        path = self.customObjects[attrs['class']].path
711
 
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
712
 
            if path:
713
 
                menu = gtk.Menu()
714
 
                menu_items = gtk.ImageMenuItem(_('_Save as...'))
715
 
                menu_items.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE,
716
 
                    gtk.ICON_SIZE_MENU))
717
 
                menu.append(menu_items)
718
 
                menu_items.connect('activate', self.customEmoticonSave, path,
719
 
                    attrs['data'])
720
 
                menu_items.show()
721
 
                menu.popup(None, None, None, event.button, event.time)
722
 
 
723
 
    def customEmoticonSave( self, widget , path , shortcut ):
724
 
        def _on_ce_edit_cb(response, text=''):
725
 
            '''method called when the edition is done'''
726
 
 
727
 
            if response == stock.ACCEPT:
728
 
                if text:
729
 
                    success, msg = self.controller.customEmoticons.create(\
730
 
                        text, path, 1)
731
 
 
732
 
                    if not success:
733
 
                        dialog.error(msg)
734
 
                else:
735
 
                    dialog.error(_("Empty shortcut"))
736
 
 
737
 
        window = dialog.entry_window(_("New shortcut"), shortcut,
738
 
            _on_ce_edit_cb, _("Shortcut"))
739
 
        window.show()
740
 
 
741
 
    def ink( self, textview, id, path=None, alttext=None):
742
 
        img = gtk.Image()
743
 
        img.set_from_pixbuf( gtk.gdk.pixbuf_new_from_file( id ) )
744
 
        img.show()
745
 
        return img
746
 
 
747
 
    def setCustomObject(self, id, path=None, type=None, alternate_text=None):
748
 
        if type in self.customObjectsCallbacks:
749
 
            return self.customObjectsCallbacks[type][0](self, id, path, alternate_text)
750
 
 
751
 
    def getCustomObjects(self):
752
 
        return self.customObjects
753
 
 
754
 
    def on_link_clicked(self, view, url, type_):
755
 
        desktop.open(url)
756
 
 
757
 
    def __leave_event(self, widget, event):
758
 
        if self._changed_cursor:
759
 
            window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
760
 
            window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
761
 
            self._changed_cursor = False
762
 
 
763
 
    def __motion_notify_event(self, widget, event):
764
 
        x, y, _ = widget.window.get_pointer()
765
 
        x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
766
 
        tags = widget.get_iter_at_location(x, y).get_tags()
767
 
        for tag in tags:
768
 
            if getattr(tag, 'is_anchor', False):
769
 
                is_over_anchor = True
770
 
                break
771
 
        else:
772
 
            is_over_anchor = False
773
 
        if not self._changed_cursor and is_over_anchor:
774
 
            window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
775
 
            window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
776
 
            self._changed_cursor = True
777
 
        elif self._changed_cursor and not is_over_anchor:
778
 
            window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
779
 
            window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
780
 
            self._changed_cursor = False
781
 
        return False
782
 
 
783
 
    def set_background(self, pixbuf):
784
 
        '''Sets a background pixbuf to this htmltextview
785
 
        If you are using this textview inside a ScrolledWindow, remember to
786
 
        pass the "scrolledwin" param to the constructor'''
787
 
 
788
 
        # converts the pixbuf to a server side pixmap
789
 
        pixmap = pixbuf.render_pixmap_and_mask()[0]
790
 
 
791
 
        # gets the text window
792
 
        textwnd = self.get_window(gtk.TEXT_WINDOW_TEXT)
793
 
 
794
 
        # sets background pixmap
795
 
        style = self.get_style()
796
 
        style.bg_pixmap[gtk.STATE_NORMAL] = pixmap
797
 
        style.set_background(textwnd, gtk.STATE_NORMAL)
798
 
 
799
 
        # this works too, but it doesn't apply background under scrollbars
800
 
        #textwnd.set_back_pixmap(pixmap, False)
801
 
 
802
 
    def display_html(self, htmlu):
803
 
 
804
 
        #####################################################################
805
 
        # handle WLM's fucking special chars, otherwise htmltextview breaks #
806
 
        # let me know if you find other fucking unicode shit crap           #
807
 
        #####################################################################
808
 
        html = htmlu.\
809
 
                replace('\x02', '<span style="font-weight: bold;">').\
810
 
                replace('\x1f', '<span style="text-decoration:underline">').\
811
 
                replace('\x0f', '').\
812
 
                replace('\x03', '').\
813
 
                replace('\x04', '').\
814
 
                replace('\x08', '')
815
 
 
816
 
        buffer = self.get_buffer()
817
 
        eob = buffer.get_end_iter()
818
 
        parser = xml.parsers.expat.ParserCreate()
819
 
        handler = HtmlHandler(self, eob, self.canResize)
820
 
        parser.StartElementHandler = handler.start_element
821
 
        parser.EndElementHandler = handler.end_element
822
 
        parser.CharacterDataHandler = handler.char_data
823
 
        parser.Parse(html)
824
 
        
825
 
        if not eob.starts_line():
826
 
            buffer.insert(eob, "\n")
827
 
 
828
 
    def scrollToBottom(self, force=False):
829
 
        if not force and self.isScrollLocked():
830
 
            return False
831
 
        textbuffer = self.get_buffer()
832
 
        textiter = textbuffer.get_end_iter()
833
 
        mark = textbuffer.create_mark("end", textiter, False)
834
 
        self._last_mark = mark
835
 
        self.scroll_to_mark(mark, 0.05, True, 0.0, 1.0)
836
 
        textbuffer.place_cursor(textiter)
837
 
        return False
838
 
 
839
 
    def isScrollLocked(self):
840
 
        textiter = self.get_buffer().get_end_iter()
841
 
        if self._last_mark:
842
 
            rect = self.get_visible_rect()
843
 
            pos = rect.y + rect.height
844
 
            if (pos + 25) < self.get_iter_location(textiter).y:
845
 
                return True
846
 
        return False
847
 
 
848
 
    def scrollLater(self):
849
 
        if not self.isScrollLocked():
850
 
            # very ugly but quite effective.
851
 
            gobject.timeout_add(200, self.scrollToBottom, True)
852
 
 
853
 
    def _on_copy_clipboard(self, textview):
854
 
        ''' replaces the copied text with a new text with the
855
 
        alt text of the images selected at copying '''
856
 
        buffer = textview.get_buffer()
857
 
        if buffer.get_has_selection():
858
 
            iterStart, iterEnd = buffer.get_selection_bounds()
859
 
            if iterStart.get_offset() > iterEnd.get_offset():
860
 
                temp = start.copy()
861
 
                iterStart = iterEnd.copy()
862
 
                iterEnd = temp.copy() #set the right begining
863
 
 
864
 
            text = buffer.get_slice(iterStart, iterEnd)
865
 
 
866
 
            #TODO: magic string '\xef\xbf\xbc', object replacement special char
867
 
            while iterStart.forward_search('\xef\xbf\xbc', \
868
 
                             gtk.TEXT_SEARCH_VISIBLE_ONLY):
869
 
 
870
 
                iterPos, iterEnd = iterStart.forward_search('\xef\xbf\xbc', \
871
 
                                  gtk.TEXT_SEARCH_VISIBLE_ONLY)
872
 
                anchor = iterPos.get_child_anchor()
873
 
                if anchor and anchor.get_widgets():
874
 
                    widget = anchor.get_widgets()[0]
875
 
                    if isinstance(widget, gtk.EventBox):
876
 
                        alt = anchor.get_widgets()[0].child.get_tooltip_text()
877
 
                    else:
878
 
                        alt = anchor.get_widgets()[0].get_tooltip_text()
879
 
                elif anchor is None:
880
 
                    alt = ''
881
 
                
882
 
                text = text.replace('\xef\xbf\xbc', alt, 1)
883
 
 
884
 
                iterStart = iterEnd
885
 
 
886
 
            clipboard = gtk.clipboard_get()
887
 
            clipboard.set_text(text)
888
 
            clipboard.store()
889
 
 
890
 
    def clear(self):
891
 
        self.get_buffer().set_text('')
892
 
        self.emotes = {}
893
 
            
894
 
if __name__ == '__main__':
895
 
    sw = gtk.ScrolledWindow()
896
 
    htmlview = HtmlTextView(scrolledwin=sw)
897
 
    def url_cb(view, url, type_):
898
 
        print "url-clicked", url, type_
899
 
    htmlview.connect("url-clicked", url_cb)
900
 
 
901
 
    #~ htmlview.display_html('<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n'
902
 
                          #~ '  <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
903
 
                          #~ '  <span style="font-size: 500%; font-family: serif">World</span>\n'
904
 
                          #~ '</div>\n')
905
 
    #~ htmlview.display_html("<br/>")
906
 
    #~ htmlview.display_html("""
907
 
      #~ <p style='font-size:large'>
908
 
        #~ <span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
909
 
        #~ I&apos;m <span style='color:green'>green</span>
910
 
        #~ with <span style='font-weight: bold'>envy</span>!
911
 
      #~ </p>
912
 
        #~ """)
913
 
    #~ htmlview.display_html("<br/>")
914
 
    #~ htmlview.display_html("""
915
 
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
916
 
      #~ <p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
917
 
      #~ <p style='margin-left: 5px; margin-right: 2%'>
918
 
        #~ &quot;A foolish consistency is the hobgoblin of little minds.&quot;
919
 
      #~ </p>
920
 
    #~ </body>
921
 
        #~ """)
922
 
    #~ htmlview.display_html("<br/>")
923
 
    #~ htmlview.display_html("""
924
 
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
925
 
      #~ <p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p>
926
 
      #~ <p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg'
927
 
              #~ alt='A License to Jabber'
928
 
              #~ height='261'
929
 
              #~ width='537'/></p>
930
 
    #~ </body>
931
 
        #~ """)
932
 
 
933
 
    #~ htmlview.display_html("""
934
 
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
935
 
      #~ <ul style='background-color:rgb(120,140,100)'>
936
 
       #~ <li> One </li>
937
 
       #~ <li> Two </li>
938
 
       #~ <li> Three </li>
939
 
      #~ </ul>
940
 
    #~ </body>
941
 
        #~ """)
942
 
    #~ htmlview.display_html("""
943
 
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
944
 
      #~ <ol>
945
 
       #~ <li> One </li>
946
 
       #~ <li> Two </li>
947
 
       #~ <li> Three </li>
948
 
      #~ </ol>
949
 
    #~ </body>
950
 
        #~ """)
951
 
    htmlview.display_html('<body>If you are reading this, you may be ' \
952
 
        'interested in that useless text that came with htmltextview. ' \
953
 
        'Try uncommenting the code above this line.</body>')
954
 
 
955
 
    htmlview.show()
956
 
 
957
 
    sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC)
958
 
    sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC)
959
 
    sw.set_property("border-width", 0)
960
 
    sw.add(htmlview)
961
 
    sw.show()
962
 
    frame = gtk.Frame()
963
 
    frame.set_shadow_type(gtk.SHADOW_IN)
964
 
    frame.show()
965
 
    frame.add( sw )
966
 
    w = gtk.Window()
967
 
    w.add(frame)
968
 
    w.set_default_size(400, 300)
969
 
    w.show_all()
970
 
 
971
 
    # set background
972
 
    #pixbuf = gtk.gdk.pixbuf_new_from_file("Auto.jpg")
973
 
    #htmlview.set_background(pixbuf)
974
 
 
975
 
    w.connect("destroy", lambda w: gtk.main_quit())
976
 
    gtk.main()