correctly set message as read when we print them in chat control
[gajim.git] / src / htmltextview.py
blob6a9e58fb34cf07c208b3bfe84cfdc5d8a9d1cab5
1 # -*- coding:utf-8 -*-
2 ## src/htmltextview.py
3 ##
4 ## Copyright (C) 2005 Gustavo J. A. M. Carneiro
5 ## Copyright (C) 2006 Santiago Gala
6 ## Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
7 ## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
8 ## Copyright (C) 2007 Nikos Kouremenos <kourem AT gmail.com>
9 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
10 ## Julien Pivotto <roidelapluie AT gmail.com>
11 ## Stephan Erb <steve-e AT h3c.de>
13 ## This file is part of Gajim.
15 ## Gajim is free software; you can redistribute it and/or modify
16 ## it under the terms of the GNU General Public License as published
17 ## by the Free Software Foundation; version 3 only.
19 ## Gajim is distributed in the hope that it will be useful,
20 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ## GNU General Public License for more details.
24 ## You should have received a copy of the GNU General Public License
25 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
28 """
29 A gtk.TextView-based renderer for XHTML-IM, as described in:
30 http://xmpp.org/extensions/xep-0071.html
32 Starting with the version posted by Gustavo Carneiro,
33 I (Santiago Gala) am trying to make it more compatible
34 with the markup that docutils generate, and also more
35 modular.
36 """
38 import gobject
39 import pango
40 import gtk
41 import xml.sax, xml.sax.handler
42 import re
43 from cStringIO import StringIO
44 import socket
45 import time
46 import urllib2
47 import operator
49 if __name__ == '__main__':
50 from common import i18n
51 import common.configpaths
52 common.configpaths.gajimpaths.init_profile()
53 common.configpaths.gajimpaths.init(None)
54 import gtkgui_helpers
55 from common import gajim
56 from gtkgui_helpers import get_icon_pixmap
58 import tooltips
59 import logging
60 log = logging.getLogger('gajim.htmlview')
62 __all__ = ['HtmlTextView']
64 whitespace_rx = re.compile('\\s+')
65 allwhitespace_rx = re.compile('^\\s*$')
67 # pixels = points * display_resolution
68 display_resolution = 0.3514598*(gtk.gdk.screen_height() /
69 float(gtk.gdk.screen_height_mm()))
71 # embryo of CSS classes
72 classes = {
73 #'system-message':';display: none',
74 'problematic': ';color: red',
77 # styles for elements
78 element_styles = {
79 'u' : ';text-decoration: underline',
80 'em' : ';font-style: oblique',
81 'cite' : '; background-color:rgb(170,190,250); font-style: oblique',
82 'li' : '; margin-left: 1em; margin-right: 10%',
83 'strong' : ';font-weight: bold',
84 'pre' : '; background-color:rgb(190,190,190); font-family: monospace; white-space: pre; margin-left: 1em; margin-right: 10%',
85 'kbd' : ';background-color:rgb(210,210,210);font-family: monospace',
86 'blockquote': '; background-color:rgb(170,190,250); margin-left: 2em; margin-right: 10%',
87 'dt' : ';font-weight: bold; font-style: oblique',
88 'dd' : ';margin-left: 2em; font-style: oblique'
90 # no difference for the moment
91 element_styles['dfn'] = element_styles['em']
92 element_styles['var'] = element_styles['em']
93 # deprecated, legacy, presentational
94 element_styles['tt'] = element_styles['kbd']
95 element_styles['i'] = element_styles['em']
96 element_styles['b'] = element_styles['strong']
98 # ==========
99 # XEP-0071
100 # ==========
102 # This Integration Set includes a subset of the modules defined for
103 # XHTML 1.0 but does not redefine any existing modules, nor
104 # does it define any new modules. Specifically, it includes the
105 # following modules only:
107 # - Structure
108 # - Text
110 # * Block
112 # phrasal
113 # addr, blockquote, pre
114 # Struc
115 # div,p
116 # Heading
117 # h1, h2, h3, h4, h5, h6
119 # * Inline
121 # phrasal
122 # abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
123 # structural
124 # br, span
126 # - Hypertext (a)
127 # - List (ul, ol, dl)
128 # - Image (img)
129 # - Style Attribute
131 # Therefore XHTML-IM uses the following content models:
133 # Block.mix
134 # Block-like elements, e.g., paragraphs
135 # Flow.mix
136 # Any block or inline elements
137 # Inline.mix
138 # Character-level elements
139 # InlineNoAnchor.class
140 # Anchor element
141 # InlinePre.mix
142 # Pre element
144 # XHTML-IM also uses the following Attribute Groups:
146 # Core.extra.attrib
147 # TBD
148 # I18n.extra.attrib
149 # TBD
150 # Common.extra
151 # style
154 # ...
155 # block level:
156 # Heading h
157 # ( pres = h1 | h2 | h3 | h4 | h5 | h6 )
158 # Block ( phrasal = address | blockquote | pre )
159 # NOT ( presentational = hr )
160 # ( structural = div | p )
161 # other: section
162 # Inline ( phrasal = abbr | acronym | cite | code | dfn | em |
163 # kbd | q | samp | strong | var )
164 # NOT ( presentational = b | big | i | small | sub | sup | tt )
165 # ( structural = br | span )
166 # Param/Legacy param, font, basefont, center, s, strike, u, dir, menu,
167 # isindex
169 BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ))
170 BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', ))
171 BLOCK_PRES = set(( 'hr', )) #not in xhtml-im
172 BLOCK_STRUCT = set(( 'div', 'p', ))
173 BLOCK_HACKS = set(( 'table', 'tr' )) # at the very least, they will start line ;)
174 BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS)
176 INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', '))
177 INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im
178 INLINE_STRUCT = set('br, span'.split(', '))
179 INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT)
181 LIST_ELEMS = set( 'dl, ol, ul'.split(', '))
183 for name in BLOCK_HEAD:
184 num = eval(name[1])
185 size = (num-1) // 2
186 weigth = (num - 1) % 2
187 element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size],
188 ('font-weight: bold', 'font-style: oblique')[weigth],)
190 def _parse_css_color(color):
191 if color.startswith('rgb(') and color.endswith(')'):
192 r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
193 return gtk.gdk.Color(r, g, b)
194 else:
195 return gtk.gdk.color_parse(color)
197 def style_iter(style):
198 return ([x.strip() for x in item.split(':', 1)] for item in style.split(';')\
199 if len(item.strip()))
202 class HtmlHandler(xml.sax.handler.ContentHandler):
204 A handler to display html to a gtk textview
206 It keeps a stack of "style spans" (start/end element pairs) and a stack of
207 list counters, for nested lists.
209 def __init__(self, conv_textview, startiter):
210 xml.sax.handler.ContentHandler.__init__(self)
211 self.textbuf = conv_textview.tv.get_buffer()
212 self.textview = conv_textview.tv
213 self.iter = startiter
214 self.conv_textview = conv_textview
215 self.text = ''
216 self.starting=True
217 self.preserve = False
218 self.styles = [] # a gtk.TextTag or None, for each span level
219 self.list_counters = [] # stack (top at head) of list
220 # counters, or None for unordered list
222 def _parse_style_color(self, tag, value):
223 color = _parse_css_color(value)
224 tag.set_property('foreground-gdk', color)
226 def _parse_style_background_color(self, tag, value):
227 color = _parse_css_color(value)
228 tag.set_property('background-gdk', color)
229 tag.set_property('paragraph-background-gdk', color)
232 def _get_current_attributes(self):
233 attrs = self.textview.get_default_attributes()
234 self.iter.backward_char()
235 self.iter.get_attributes(attrs)
236 self.iter.forward_char()
237 return attrs
239 def __parse_length_frac_size_allocate(self, textview, allocation, frac,
240 callback, args):
241 callback(allocation.width*frac, *args)
243 def _parse_length(self, value, font_relative, block_relative, minl, maxl,
244 callback, *args):
246 Parse/calc length, converting to pixels, calls callback(length, *args)
247 when the length is first computed or changes
249 if value.endswith('%'):
250 val = float(value[:-1])
251 sign = cmp(val, 0)
252 # limits: 1% to 500%
253 val = sign*max(1, min(abs(val), 500))
254 frac = val/100
255 if font_relative:
256 attrs = self._get_current_attributes()
257 font_size = attrs.font.get_size() / pango.SCALE
258 callback(frac*display_resolution*font_size, *args)
259 elif block_relative:
260 # CSS says 'Percentage values: refer to width of the closest
261 # block-level ancestor'
262 # This is difficult/impossible to implement, so we use
263 # textview width instead; a reasonable approximation..
264 alloc = self.textview.get_allocation()
265 self.__parse_length_frac_size_allocate(self.textview, alloc,
266 frac, callback, args)
267 self.textview.connect('size-allocate',
268 self.__parse_length_frac_size_allocate,
269 frac, callback, args)
270 else:
271 callback(frac, *args)
272 return
274 def get_val():
275 val = float(value[:-2])
276 sign = cmp(val, 0)
277 # validate length
278 return sign*max(minl, min(abs(val*display_resolution), maxl))
279 if value.endswith('pt'): # points
280 callback(get_val()*display_resolution, *args)
282 elif value.endswith('em'): # ems, the width of the element's font
283 attrs = self._get_current_attributes()
284 font_size = attrs.font.get_size() / pango.SCALE
285 callback(get_val()*display_resolution*font_size, *args)
287 elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
288 # FIXME: figure out how to calculate this correctly
289 # for now 'em' size is used as approximation
290 attrs = self._get_current_attributes()
291 font_size = attrs.font.get_size() / pango.SCALE
292 callback(get_val()*display_resolution*font_size, *args)
294 elif value.endswith('px'): # pixels
295 callback(get_val(), *args)
297 else:
298 try:
299 # TODO: isn't "no units" interpreted as pixels?
300 val = int(value)
301 sign = cmp(val, 0)
302 # validate length
303 val = sign*max(minl, min(abs(val), maxl))
304 callback(val, *args)
305 except Exception:
306 log.warning('Unable to parse length value "%s"' % value)
308 def __parse_font_size_cb(length, tag):
309 tag.set_property('size-points', length/display_resolution)
310 __parse_font_size_cb = staticmethod(__parse_font_size_cb)
312 def _parse_style_display(self, tag, value):
313 if value == 'none':
314 tag.set_property('invisible', 'true')
315 # FIXME: display: block, inline
317 def _parse_style_font_size(self, tag, value):
318 try:
319 scale = {
320 'xx-small': pango.SCALE_XX_SMALL,
321 'x-small': pango.SCALE_X_SMALL,
322 'small': pango.SCALE_SMALL,
323 'medium': pango.SCALE_MEDIUM,
324 'large': pango.SCALE_LARGE,
325 'x-large': pango.SCALE_X_LARGE,
326 'xx-large': pango.SCALE_XX_LARGE,
327 } [value]
328 except KeyError:
329 pass
330 else:
331 attrs = self._get_current_attributes()
332 tag.set_property('scale', scale / attrs.font_scale)
333 return
334 if value == 'smaller':
335 tag.set_property('scale', pango.SCALE_SMALL)
336 return
337 if value == 'larger':
338 tag.set_property('scale', pango.SCALE_LARGE)
339 return
340 # font relative (5 ~ 4pt, 110 ~ 72pt)
341 self._parse_length(value, True, False, 5, 110,self.__parse_font_size_cb,
342 tag)
344 def _parse_style_font_style(self, tag, value):
345 try:
346 style = {
347 'normal': pango.STYLE_NORMAL,
348 'italic': pango.STYLE_ITALIC,
349 'oblique': pango.STYLE_OBLIQUE,
350 } [value]
351 except KeyError:
352 log.warning('unknown font-style %s' % value)
353 else:
354 tag.set_property('style', style)
356 def __frac_length_tag_cb(self, length, tag, propname):
357 styles = self._get_style_tags()
358 if styles:
359 length += styles[-1].get_property(propname)
360 tag.set_property(propname, length)
361 #__frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
363 def _parse_style_margin_left(self, tag, value):
364 # block relative
365 self._parse_length(value, False, True, 1, 1000,
366 self.__frac_length_tag_cb, tag, 'left-margin')
368 def _parse_style_margin_right(self, tag, value):
369 # block relative
370 self._parse_length(value, False, True, 1, 1000,
371 self.__frac_length_tag_cb, tag, 'right-margin')
373 def _parse_style_font_weight(self, tag, value):
374 # TODO: missing 'bolder' and 'lighter'
375 try:
376 weight = {
377 '100': pango.WEIGHT_ULTRALIGHT,
378 '200': pango.WEIGHT_ULTRALIGHT,
379 '300': pango.WEIGHT_LIGHT,
380 '400': pango.WEIGHT_NORMAL,
381 '500': pango.WEIGHT_NORMAL,
382 '600': pango.WEIGHT_BOLD,
383 '700': pango.WEIGHT_BOLD,
384 '800': pango.WEIGHT_ULTRABOLD,
385 '900': pango.WEIGHT_HEAVY,
386 'normal': pango.WEIGHT_NORMAL,
387 'bold': pango.WEIGHT_BOLD,
388 } [value]
389 except KeyError:
390 log.warning('unknown font-style %s' % value)
391 else:
392 tag.set_property('weight', weight)
394 def _parse_style_font_family(self, tag, value):
395 tag.set_property('family', value)
397 def _parse_style_text_align(self, tag, value):
398 try:
399 align = {
400 'left': gtk.JUSTIFY_LEFT,
401 'right': gtk.JUSTIFY_RIGHT,
402 'center': gtk.JUSTIFY_CENTER,
403 'justify': gtk.JUSTIFY_FILL,
404 } [value]
405 except KeyError:
406 log.warning('Invalid text-align:%s requested' % value)
407 else:
408 tag.set_property('justification', align)
410 def _parse_style_text_decoration(self, tag, value):
411 values = value.split(' ')
412 if 'none' in values:
413 tag.set_property('underline', pango.UNDERLINE_NONE)
414 tag.set_property('strikethrough', False)
415 if 'underline' in values:
416 tag.set_property('underline', pango.UNDERLINE_SINGLE)
417 else:
418 tag.set_property('underline', pango.UNDERLINE_NONE)
419 if 'line-through' in values:
420 tag.set_property('strikethrough', True)
421 else:
422 tag.set_property('strikethrough', False)
423 if 'blink' in values:
424 log.warning('text-decoration:blink not implemented')
425 if 'overline' in values:
426 log.warning('text-decoration:overline not implemented')
428 def _parse_style_white_space(self, tag, value):
429 if value == 'pre':
430 tag.set_property('wrap_mode', gtk.WRAP_NONE)
431 elif value == 'normal':
432 tag.set_property('wrap_mode', gtk.WRAP_WORD)
433 elif value == 'nowrap':
434 tag.set_property('wrap_mode', gtk.WRAP_NONE)
436 def __length_tag_cb(self, value, tag, propname):
437 try:
438 tag.set_property(propname, value)
439 except Exception:
440 log.warning( "Error with prop: " + propname + " for tag: " + str(tag))
443 def _parse_style_width(self, tag, value):
444 if value == 'auto':
445 return
446 self._parse_length(value, False, False, 1, 1000, self.__length_tag_cb,
447 tag, "width")
448 def _parse_style_height(self, tag, value):
449 if value == 'auto':
450 return
451 self._parse_length(value, False, False, 1, 1000, self.__length_tag_cb,
452 tag, "height")
455 # build a dictionary mapping styles to methods, for greater speed
456 __style_methods = dict()
457 for style in ('background-color', 'color', 'font-family', 'font-size',
458 'font-style', 'font-weight', 'margin-left', 'margin-right',
459 'text-align', 'text-decoration', 'white-space', 'display',
460 'width', 'height' ):
461 try:
462 method = locals()['_parse_style_%s' % style.replace('-', '_')]
463 except KeyError:
464 log.warning('Style attribute "%s" not yet implemented' % style)
465 else:
466 __style_methods[style] = method
467 del style
468 # --
470 def _get_style_tags(self):
471 return [tag for tag in self.styles if tag is not None]
473 def _create_url(self, href, title, type_, id_):
474 '''Process a url tag.
476 tag = self.textbuf.create_tag(id_)
477 if href and href[0] != '#':
478 tag.href = href
479 tag.type_ = type_ # to be used by the URL handler
480 tag.connect('event', self.textview.hyperlink_handler, 'url')
481 tag.set_property('foreground', gajim.config.get('urlmsgcolor'))
482 tag.set_property('underline', pango.UNDERLINE_SINGLE)
483 tag.is_anchor = True
484 if title:
485 tag.title = title
486 return tag
488 def _get_img(self, attrs):
489 '''Download an image. This function is launched in a separate thread.
491 mem, alt = '', ''
492 # Wait maximum 5s for connection
493 socket.setdefaulttimeout(5)
494 try:
495 req = urllib2.Request(attrs['src'])
496 req.add_header('User-Agent', 'Gajim ' + gajim.version)
497 f = urllib2.urlopen(req)
498 except Exception, ex:
499 log.debug('Error loading image %s ' % attrs['src'] + str(ex))
500 pixbuf = None
501 alt = attrs.get('alt', 'Broken image')
502 else:
503 # Wait 0.5s between each byte
504 try:
505 f.fp._sock.fp._sock.settimeout(0.5)
506 except Exception:
507 pass
508 # Max image size = 2 MB (to try to prevent DoS)
509 deadline = time.time() + 3
510 while True:
511 if time.time() > deadline:
512 log.debug(str('Timeout loading image %s ' % \
513 attrs['src'] + ex))
514 mem = ''
515 alt = attrs.get('alt', '')
516 if alt:
517 alt += '\n'
518 alt += _('Timeout loading image')
519 break
520 try:
521 temp = f.read(100)
522 except socket.timeout, ex:
523 log.debug('Timeout loading image %s ' % \
524 attrs['src'] + str(ex))
525 alt = attrs.get('alt', '')
526 if alt:
527 alt += '\n'
528 alt += _('Timeout loading image')
529 break
530 if temp:
531 mem += temp
532 else:
533 break
534 if len(mem) > 2*1024*1024:
535 alt = attrs.get('alt', '')
536 if alt:
537 alt += '\n'
538 alt += _('Image is too big')
539 break
540 return (mem, alt)
542 def _update_img(self, (mem, alt), attrs, img_mark):
543 '''Callback function called after the function _get_img above.
545 self._process_img(attrs, (mem, alt, img_mark))
547 def _process_img(self, attrs, loaded=None):
548 '''Process a img tag.
550 mem = ''
551 update = False
552 pixbuf = None
553 replace_mark = None
555 try:
556 if attrs['src'].startswith('data:image/'):
557 # The "data" URL scheme http://tools.ietf.org/html/rfc2397
558 import base64
559 img = attrs['src'].split(',')[1]
560 mem = base64.standard_b64decode(urllib2.unquote(img))
561 elif loaded is not None:
562 (mem, alt, replace_mark) = loaded
563 update = True
564 else:
565 img_mark = self.textbuf.create_mark(None, self.iter, True)
566 gajim.thread_interface(self._get_img, [attrs], \
567 self._update_img, [attrs, img_mark])
568 alt = attrs.get('alt', '')
569 if alt:
570 alt += '\n'
571 alt += _('Loading')
572 pixbuf = get_icon_pixmap('gajim-receipt_missing')
573 if mem:
574 # Caveat: GdkPixbuf is known not to be safe to load
575 # images from network... this program is now potentially
576 # hackable ;)
577 loader = gtk.gdk.PixbufLoader()
578 dims = [0, 0]
579 def height_cb(length):
580 dims[1] = length
581 def width_cb(length):
582 dims[0] = length
583 # process width and height attributes
584 w = attrs.get('width')
585 h = attrs.get('height')
586 # override with width and height styles
587 for attr, val in style_iter(attrs.get('style', '')):
588 if attr == 'width':
589 w = val
590 elif attr == 'height':
591 h = val
592 if w:
593 self._parse_length(w, False, False, 1, 1000, width_cb)
594 if h:
595 self._parse_length(h, False, False, 1, 1000, height_cb)
596 def set_size(pixbuf, w, h, dims):
598 FIXME: Floats should be relative to the whole textview, and
599 resize with it. This needs new pifbufs for every resize,
600 gtk.gdk.Pixbuf.scale_simple or similar.
602 if isinstance(dims[0], float):
603 dims[0] = int(dims[0]*w)
604 elif not dims[0]:
605 dims[0] = w
606 if isinstance(dims[1], float):
607 dims[1] = int(dims[1]*h)
608 if not dims[1]:
609 dims[1] = h
610 loader.set_size(*dims)
611 if w or h:
612 loader.connect('size-prepared', set_size, dims)
613 loader.write(mem)
614 loader.close()
615 pixbuf = loader.get_pixbuf()
616 alt = attrs.get('alt', '')
617 working_iter = self.iter
618 if replace_mark is not None:
619 working_iter = self.textbuf.get_iter_at_mark(replace_mark)
620 next_iter = working_iter.copy()
621 next_iter.forward_char()
622 self.textbuf.delete(working_iter, next_iter)
623 self.textbuf.delete_mark(replace_mark)
624 if pixbuf is not None:
625 tags = self._get_style_tags()
626 if tags:
627 tmpmark = self.textbuf.create_mark(None, working_iter, True)
628 self.textbuf.insert_pixbuf(working_iter, pixbuf)
629 self.starting = False
630 if tags:
631 start = self.textbuf.get_iter_at_mark(tmpmark)
632 for tag in tags:
633 self.textbuf.apply_tag(tag, start, working_iter)
634 self.textbuf.delete_mark(tmpmark)
635 else:
636 self._insert_text('[IMG: %s]' % alt, working_iter)
637 except Exception, ex:
638 log.error('Error loading image ' + str(ex))
639 pixbuf = None
640 alt = attrs.get('alt', 'Broken image')
641 try:
642 loader.close()
643 except Exception:
644 pass
645 return pixbuf
647 def _begin_span(self, style, tag=None, id_=None):
648 if style is None:
649 self.styles.append(tag)
650 return None
651 if tag is None:
652 if id_:
653 tag = self.textbuf.create_tag(id_)
654 else:
655 tag = self.textbuf.create_tag() # we create anonymous tag
656 for attr, val in style_iter(style):
657 attr = attr.lower()
658 val = val
659 try:
660 method = self.__style_methods[attr]
661 except KeyError:
662 log.warning('Style attribute "%s" requested '
663 'but not yet implemented' % attr)
664 else:
665 method(self, tag, val)
666 self.styles.append(tag)
668 def _end_span(self):
669 self.styles.pop()
671 def _jump_line(self):
672 self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
673 self.starting = True
675 def _insert_text(self, text, working_iter=None):
676 if working_iter == None:
677 working_iter = self.iter
678 if self.starting and text != '\n':
679 self.starting = (text[-1] == '\n')
680 tags = self._get_style_tags()
681 if tags:
682 self.textbuf.insert_with_tags(working_iter, text, *tags)
683 else:
684 self.textbuf.insert(working_iter, text)
686 def _starts_line(self):
687 return self.starting or self.iter.starts_line()
689 def _flush_text(self):
690 if not self.text: return
691 text, self.text = self.text, ''
692 if not self.preserve:
693 text = text.replace('\n', ' ')
694 self.handle_specials(whitespace_rx.sub(' ', text))
695 else:
696 self._insert_text(text.strip('\n'))
698 def _anchor_event(self, tag, textview, event, iter_, href, type_):
699 if event.type == gtk.gdk.BUTTON_PRESS:
700 self.textview.emit('url-clicked', href, type_)
701 return True
702 return False
704 def handle_specials(self, text):
705 self.iter = self.conv_textview.detect_and_print_special_text(text,
706 self._get_style_tags())
708 def characters(self, content):
709 if self.preserve:
710 self.text += content
711 return
712 if allwhitespace_rx.match(content) is not None and self._starts_line():
713 return
714 self.text += content
715 self.starting = False
718 def startElement(self, name, attrs):
719 self._flush_text()
720 klass = [i for i in attrs.get('class', ' ').split(' ') if i]
721 style = ''
722 #Add styles defined for classes
723 for k in klass:
724 if k in classes:
725 style += classes[k]
727 tag = None
728 #FIXME: if we want to use id, it needs to be unique across
729 # the whole textview, so we need to add something like the
730 # message-id to it.
731 #id_ = attrs.get('id',None)
732 id_ = None
733 if name == 'a':
734 #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
735 href = attrs.get('href', None)
736 if not href:
737 href = attrs.get('HREF', None)
738 # Gaim sends HREF instead of href
739 title = attrs.get('title', attrs.get('rel', href))
740 type_ = attrs.get('type', None)
741 tag = self._create_url(href, title, type_, id_)
742 elif name == 'blockquote':
743 cite = attrs.get('cite', None)
744 if cite:
745 tag = self.textbuf.create_tag(id_)
746 tag.title = title
747 tag.is_anchor = True
748 elif name in LIST_ELEMS:
749 style += ';margin-left: 2em'
750 elif name == 'img':
751 tag = self._process_img(attrs)
752 if name in element_styles:
753 style += element_styles[name]
754 # so that explicit styles override implicit ones,
755 # we add the attribute last
756 style += ";"+attrs.get('style', '')
757 if style == '':
758 style = None
759 self._begin_span(style, tag, id_)
761 if name == 'br':
762 pass # handled in endElement
763 elif name == 'hr':
764 pass # handled in endElement
765 elif name in BLOCK:
766 if not self._starts_line():
767 self._jump_line()
768 if name == 'pre':
769 self.preserve = True
770 elif name == 'span':
771 pass
772 elif name in ('dl', 'ul'):
773 if not self._starts_line():
774 self._jump_line()
775 self.list_counters.append(None)
776 elif name == 'ol':
777 if not self._starts_line():
778 self._jump_line()
779 self.list_counters.append(0)
780 elif name == 'li':
781 if self.list_counters[-1] is None:
782 li_head = unichr(0x2022)
783 else:
784 self.list_counters[-1] += 1
785 li_head = '%i.' % self.list_counters[-1]
786 self.text = ' '*len(self.list_counters)*4 + li_head + ' '
787 self._flush_text()
788 self.starting = True
789 elif name == 'dd':
790 self._jump_line()
791 elif name == 'dt':
792 if not self.starting:
793 self._jump_line()
794 elif name in ('a', 'img', 'body', 'html'):
795 pass
796 elif name in INLINE:
797 pass
798 else:
799 log.warning('Unhandled element "%s"' % name)
801 def endElement(self, name):
802 endPreserving = False
803 newLine = False
804 if name == 'br':
805 newLine = True
806 elif name == 'hr':
807 #FIXME: plenty of unused attributes (width, height,...) :)
808 self._jump_line()
809 try:
810 self.textbuf.insert_pixbuf(self.iter,
811 self.textview.focus_out_line_pixbuf)
812 #self._insert_text(u'\u2550'*40)
813 self._jump_line()
814 except Exception, e:
815 log.debug(str('Error in hr'+e))
816 elif name in LIST_ELEMS:
817 self.list_counters.pop()
818 elif name == 'li':
819 newLine = True
820 elif name == 'img':
821 pass
822 elif name == 'body' or name == 'html':
823 pass
824 elif name == 'a':
825 pass
826 elif name in INLINE:
827 pass
828 elif name in ('dd', 'dt', ):
829 pass
830 elif name in BLOCK:
831 if name == 'pre':
832 endPreserving = True
833 else:
834 log.warning("Unhandled element '%s'" % name)
835 self._flush_text()
836 if endPreserving:
837 self.preserve = False
838 if newLine:
839 self._jump_line()
840 self._end_span()
841 #if not self._starts_line():
842 # self.text = ' '
844 class HtmlTextView(gtk.TextView):
846 def __init__(self):
847 gobject.GObject.__init__(self)
848 self.set_wrap_mode(gtk.WRAP_CHAR)
849 self.set_editable(False)
850 self._changed_cursor = False
851 self.connect('destroy', self.__destroy_event)
852 self.connect('motion-notify-event', self.__motion_notify_event)
853 self.connect('leave-notify-event', self.__leave_event)
854 self.connect('enter-notify-event', self.__motion_notify_event)
855 self.connect('realize', self.on_html_text_view_realized)
856 self.connect('unrealize', self.on_html_text_view_unrealized)
857 self.connect('copy-clipboard', self.on_html_text_view_copy_clipboard)
858 self.get_buffer().connect_after('mark-set', self.on_text_buffer_mark_set)
859 self.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL)
860 self.tooltip = tooltips.BaseTooltip()
861 self.config = gajim.config
862 self.interface = gajim.interface
863 # end big hack
865 def __destroy_event(self, widget):
866 if self.tooltip.timeout != 0:
867 self.tooltip.hide_tooltip()
869 def __leave_event(self, widget, event):
870 if self._changed_cursor:
871 window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
872 window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
873 self._changed_cursor = False
875 def show_tooltip(self, tag):
876 if not self.tooltip.win:
877 # check if the current pointer is still over the line
878 x, y, _ = self.window.get_pointer()
879 x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
880 tags = self.get_iter_at_location(x, y).get_tags()
881 is_over_anchor = False
882 for tag_ in tags:
883 if getattr(tag_, 'is_anchor', False):
884 is_over_anchor = True
885 break
886 if not is_over_anchor:
887 return
888 text = getattr(tag, 'title', False)
889 if text:
890 pointer = self.get_pointer()
891 position = self.window.get_origin()
892 self.tooltip.show_tooltip(text, 8, position[1] + pointer[1])
894 def __motion_notify_event(self, widget, event):
895 x, y, _ = widget.window.get_pointer()
896 x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
897 tags = widget.get_iter_at_location(x, y).get_tags()
898 anchor_tags = [tag for tag in tags if getattr(tag, 'is_anchor', False)]
899 if self.tooltip.timeout != 0:
900 # Check if we should hide the line tooltip
901 if not anchor_tags:
902 self.tooltip.hide_tooltip()
903 if not self._changed_cursor and anchor_tags:
904 window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
905 window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
906 self._changed_cursor = True
907 self.tooltip.timeout = gobject.timeout_add(500, self.show_tooltip,
908 anchor_tags[0])
909 elif self._changed_cursor and not anchor_tags:
910 window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
911 window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
912 self._changed_cursor = False
913 return False
915 def display_html(self, html, conv_textview):
916 buffer_ = self.get_buffer()
917 eob = buffer_.get_end_iter()
918 ## this works too if libxml2 is not available
919 # parser = xml.sax.make_parser(['drv_libxml2'])
920 # parser.setFeature(xml.sax.handler.feature_validation, True)
921 parser = xml.sax.make_parser()
922 parser.setContentHandler(HtmlHandler(conv_textview, eob))
923 parser.parse(StringIO(html))
925 # too much space after :)
926 #if not eob.starts_line():
927 # buffer_.insert(eob, '\n')
929 def on_html_text_view_copy_clipboard(self, unused_data):
930 clipboard = self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD)
931 clipboard.set_text(self.get_selected_text())
932 self.emit_stop_by_name('copy-clipboard')
934 def on_html_text_view_realized(self, unused_data):
935 self.get_buffer().remove_selection_clipboard(self.get_clipboard(
936 gtk.gdk.SELECTION_PRIMARY))
938 def on_html_text_view_unrealized(self, unused_data):
939 self.get_buffer().add_selection_clipboard(self.get_clipboard(
940 gtk.gdk.SELECTION_PRIMARY))
942 def on_text_buffer_mark_set(self, location, mark, unused_data):
943 bounds = self.get_buffer().get_selection_bounds()
944 if bounds:
945 # textview can be hidden while we add a new line in it.
946 if self.has_screen():
947 clipboard = self.get_clipboard(gtk.gdk.SELECTION_PRIMARY)
948 clipboard.set_text(self.get_selected_text())
950 def get_selected_text(self):
951 bounds = self.get_buffer().get_selection_bounds()
952 selection = ''
953 if bounds:
954 (search_iter, end) = bounds
956 while (search_iter.compare(end)):
957 character = search_iter.get_char()
958 if character == u'\ufffc':
959 anchor = search_iter.get_child_anchor()
960 if anchor:
961 text = anchor.get_data('plaintext')
962 if text:
963 selection+=text
964 else:
965 selection+=character
966 else:
967 selection+=character
968 search_iter.forward_char()
969 return selection
971 change_cursor = None
973 if __name__ == '__main__':
974 import os
976 from conversation_textview import ConversationTextview
977 import gajim as gaj
979 log = logging.getLogger()
980 gaj.Interface()
982 htmlview = ConversationTextview(None)
984 path = gtkgui_helpers.get_icon_path('gajim-muc_separator')
985 # use this for hr
986 htmlview.tv.focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path)
988 tooltip = tooltips.BaseTooltip()
990 def on_textview_motion_notify_event(widget, event):
992 Change the cursor to a hand when we are over a mail or an url
994 global change_cursor
995 pointer_x, pointer_y = htmlview.tv.window.get_pointer()[0:2]
996 x, y = htmlview.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
997 pointer_x, pointer_y)
998 tags = htmlview.tv.get_iter_at_location(x, y).get_tags()
999 if change_cursor:
1000 htmlview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
1001 gtk.gdk.Cursor(gtk.gdk.XTERM))
1002 change_cursor = None
1003 tag_table = htmlview.tv.get_buffer().get_tag_table()
1004 for tag in tags:
1005 try:
1006 if tag.is_anchor:
1007 htmlview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
1008 gtk.gdk.Cursor(gtk.gdk.HAND2))
1009 change_cursor = tag
1010 elif tag == tag_table.lookup('focus-out-line'):
1011 over_line = True
1012 except Exception:
1013 pass
1015 #if line_tooltip.timeout != 0:
1016 # Check if we should hide the line tooltip
1017 # if not over_line:
1018 # line_tooltip.hide_tooltip()
1019 #if over_line and not line_tooltip.win:
1020 # line_tooltip.timeout = gobject.timeout_add(500,
1021 # show_line_tooltip)
1022 # htmlview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
1023 # gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
1024 # change_cursor = tag
1026 htmlview.tv.connect('motion_notify_event', on_textview_motion_notify_event)
1028 def handler(texttag, widget, event, iter_, kind):
1029 if event.type == gtk.gdk.BUTTON_PRESS:
1030 pass
1032 htmlview.tv.hyperlink_handler = htmlview.hyperlink_handler
1034 htmlview.print_real_text(None, xhtml='<div>'
1035 '<span style="color: red; text-decoration:underline">Hello</span><br/>\n'
1036 ' <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
1037 '<span style="font-size: 500%; font-family: serif">World</span>\n'
1038 '</div>\n')
1039 htmlview.print_real_text(None, xhtml='<hr />')
1040 htmlview.print_real_text(None, xhtml='''
1041 <body xmlns='http://www.w3.org/1999/xhtml'>
1042 <p xmlns='http://www.w3.org/1999/xhtml'>a:b
1043 <a href='http://google.com/' xmlns='http://www.w3.org/1999/xhtml'>Google
1044 </a>
1045 </p><br/>
1046 </body>''')
1047 htmlview.print_real_text(None, xhtml='''
1048 <body xmlns='http://www.w3.org/1999/xhtml'>
1049 <p style='font-size:large'>
1050 <span style='font-style: italic'>O
1051 <span style='font-size:larger'>M</span>G</span>,
1052 I&apos;m <span style='color:green'>green</span>
1053 with <span style='font-weight: bold'>envy</span>!
1054 </p>
1055 </body>
1056 ''')
1057 htmlview.print_real_text(None, xhtml='<hr />')
1058 htmlview.print_real_text(None, xhtml='''
1059 <body xmlns='http://www.w3.org/1999/xhtml'>
1060 http://test.com/ testing links autolinkifying
1061 </body>
1062 ''')
1063 htmlview.print_real_text(None, xhtml='<hr />')
1064 htmlview.print_real_text(None, xhtml='''
1065 <body xmlns='http://www.w3.org/1999/xhtml'>
1066 <p>As Emerson said in his essay <span style='
1067 font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
1068 <p style='margin-left: 5px; margin-right: 2%'>
1069 &quot;A foolish consistency is the hobgoblin of little minds.&quot;
1070 </p>
1071 </body>
1072 ''')
1073 htmlview.print_real_text(None, xhtml='<hr />')
1074 htmlview.print_real_text(None, xhtml='''
1075 <body xmlns='http://www.w3.org/1999/xhtml'>
1076 <p style='text-align:center'>
1077 Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?
1078 </p>
1079 <p style='text-align:right'>
1080 <img src='http://www.xmpp.org/images/psa-license.jpg'
1081 alt='A License to Jabber' width='50%' height='50%'/>
1082 </p>
1083 </body>
1084 ''')
1085 htmlview.print_real_text(None, xhtml='<hr />')
1086 htmlview.print_real_text(None, xhtml='''
1087 <body xmlns='http://www.w3.org/1999/xhtml'>
1088 <ul style='background-color:rgb(120,140,100)'>
1089 <li> One </li>
1090 <li> Two </li>
1091 <li> Three </li>
1092 </ul><hr /><pre style="background-color:rgb(120,120,120)">def fac(n):
1093 def faciter(n,acc):
1094 if n==0: return acc
1095 return faciter(n-1, acc*n)
1096 if n&lt;0: raise ValueError('Must be non-negative')
1097 return faciter(n,1)</pre>
1098 </body>
1099 ''')
1100 htmlview.print_real_text(None, xhtml='<hr />')
1101 htmlview.print_real_text(None, xhtml='''
1102 <body xmlns='http://www.w3.org/1999/xhtml'>
1103 <ol style='background-color:rgb(120,140,100)'>
1104 <li> One </li>
1105 <li> Two is nested: <ul style='background-color:rgb(200,200,100)'>
1106 <li> One </li>
1107 <li style='font-size:50%'> Two </li>
1108 <li style='font-size:200%'> Three </li>
1109 <li style='font-size:9999pt'> Four </li>
1110 </ul></li>
1111 <li> Three </li></ol>
1112 </body>
1113 ''')
1114 htmlview.print_real_text(None, xhtml='<hr />')
1115 htmlview.print_real_text(None, xhtml='''
1116 <body xmlns='http://www.w3.org/1999/xhtml'>
1118 <strong>
1119 <a href='xmpp:example@example.org'>xmpp link</a>
1120 </strong>: </p>
1121 <div xmlns='http://www.w3.org/1999/xhtml'>
1122 <cite style='margin: 7px;' title='xmpp:examples@example.org'>
1124 <strong>examples@example.org wrote:</strong>
1125 </p>
1126 <p>this cite - bla bla bla, smile- :-) ...</p>
1127 </cite>
1128 <div>
1129 <p>some text</p>
1130 </div>
1131 </div>
1132 <p/>
1133 <p>#232/1</p>
1134 </body>
1135 ''')
1136 htmlview.print_real_text(None, xhtml='<hr />')
1137 htmlview.print_real_text(None, xhtml='''
1138 <body xmlns='http://www.w3.org/1999/xhtml'>
1139 <br/>
1140 <img src='data:image/png;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw\
1141 AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz\
1142 ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp\
1143 a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl\
1144 ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis\
1145 F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH\
1146 hhx4dbgYKAAA7' alt='Larry'/>
1147 </body>
1148 ''')
1149 htmlview.tv.show()
1150 sw = gtk.ScrolledWindow()
1151 sw.set_property('hscrollbar-policy', gtk.POLICY_AUTOMATIC)
1152 sw.set_property('vscrollbar-policy', gtk.POLICY_AUTOMATIC)
1153 sw.set_property('border-width', 0)
1154 sw.add(htmlview.tv)
1155 sw.show()
1156 frame = gtk.Frame()
1157 frame.set_shadow_type(gtk.SHADOW_IN)
1158 frame.show()
1159 frame.add(sw)
1160 w = gtk.Window()
1161 w.add(frame)
1162 w.set_default_size(400, 300)
1163 w.show_all()
1164 w.connect('destroy', lambda w: gtk.main_quit())
1165 gtk.main()