Added AgiliaLinux to known distributions. Fix #6033
[gajim.git] / src / htmltextview.py
blob4883058433e8da470b5443a8168ef36d0a96c4e6
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://www.jabber.org/jeps/jep-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 import warnings
44 from cStringIO import StringIO
45 import socket
46 import time
47 import urllib2
48 import operator
50 if __name__ == '__main__':
51 from common import i18n
52 import common.configpaths
53 common.configpaths.gajimpaths.init(None)
54 from common import gajim
56 import tooltips
59 __all__ = ['HtmlTextView']
61 whitespace_rx = re.compile('\\s+')
62 allwhitespace_rx = re.compile('^\\s*$')
64 # pixels = points * display_resolution
65 display_resolution = 0.3514598*(gtk.gdk.screen_height() /
66 float(gtk.gdk.screen_height_mm()))
68 # embryo of CSS classes
69 classes = {
70 #'system-message':';display: none',
71 'problematic': ';color: red',
74 # styles for elements
75 element_styles = {
76 'u' : ';text-decoration: underline',
77 'em' : ';font-style: oblique',
78 'cite' : '; background-color:rgb(170,190,250); font-style: oblique',
79 'li' : '; margin-left: 1em; margin-right: 10%',
80 'strong' : ';font-weight: bold',
81 'pre' : '; background-color:rgb(190,190,190); font-family: monospace; white-space: pre; margin-left: 1em; margin-right: 10%',
82 'kbd' : ';background-color:rgb(210,210,210);font-family: monospace',
83 'blockquote': '; background-color:rgb(170,190,250); margin-left: 2em; margin-right: 10%',
84 'dt' : ';font-weight: bold; font-style: oblique',
85 'dd' : ';margin-left: 2em; font-style: oblique'
87 # no difference for the moment
88 element_styles['dfn'] = element_styles['em']
89 element_styles['var'] = element_styles['em']
90 # deprecated, legacy, presentational
91 element_styles['tt'] = element_styles['kbd']
92 element_styles['i'] = element_styles['em']
93 element_styles['b'] = element_styles['strong']
95 # ==========
96 # JEP-0071
97 # ==========
99 # This Integration Set includes a subset of the modules defined for
100 # XHTML 1.0 but does not redefine any existing modules, nor
101 # does it define any new modules. Specifically, it includes the
102 # following modules only:
104 # - Structure
105 # - Text
107 # * Block
109 # phrasal
110 # addr, blockquote, pre
111 # Struc
112 # div,p
113 # Heading
114 # h1, h2, h3, h4, h5, h6
116 # * Inline
118 # phrasal
119 # abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
120 # structural
121 # br, span
123 # - Hypertext (a)
124 # - List (ul, ol, dl)
125 # - Image (img)
126 # - Style Attribute
128 # Therefore XHTML-IM uses the following content models:
130 # Block.mix
131 # Block-like elements, e.g., paragraphs
132 # Flow.mix
133 # Any block or inline elements
134 # Inline.mix
135 # Character-level elements
136 # InlineNoAnchor.class
137 # Anchor element
138 # InlinePre.mix
139 # Pre element
141 # XHTML-IM also uses the following Attribute Groups:
143 # Core.extra.attrib
144 # TBD
145 # I18n.extra.attrib
146 # TBD
147 # Common.extra
148 # style
151 # ...
152 # block level:
153 # Heading h
154 # ( pres = h1 | h2 | h3 | h4 | h5 | h6 )
155 # Block ( phrasal = address | blockquote | pre )
156 # NOT ( presentational = hr )
157 # ( structural = div | p )
158 # other: section
159 # Inline ( phrasal = abbr | acronym | cite | code | dfn | em |
160 # kbd | q | samp | strong | var )
161 # NOT ( presentational = b | big | i | small | sub | sup | tt )
162 # ( structural = br | span )
163 # Param/Legacy param, font, basefont, center, s, strike, u, dir, menu,
164 # isindex
166 BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ))
167 BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', ))
168 BLOCK_PRES = set(( 'hr', )) #not in xhtml-im
169 BLOCK_STRUCT = set(( 'div', 'p', ))
170 BLOCK_HACKS = set(( 'table', 'tr' )) # at the very least, they will start line ;)
171 BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS)
173 INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', '))
174 INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im
175 INLINE_STRUCT = set('br, span'.split(', '))
176 INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT)
178 LIST_ELEMS = set( 'dl, ol, ul'.split(', '))
180 for name in BLOCK_HEAD:
181 num = eval(name[1])
182 size = (num-1) // 2
183 weigth = (num - 1) % 2
184 element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size],
185 ('font-weight: bold', 'font-style: oblique')[weigth],
188 def _parse_css_color(color):
189 if color.startswith('rgb(') and color.endswith(')'):
190 r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
191 return gtk.gdk.Color(r, g, b)
192 else:
193 return gtk.gdk.color_parse(color)
195 def style_iter(style):
196 return ([x.strip() for x in item.split(':', 1)] for item in style.split(';')\
197 if len(item.strip()))
200 class HtmlHandler(xml.sax.handler.ContentHandler):
202 A handler to display html to a gtk textview
204 It keeps a stack of "style spans" (start/end element pairs) and a stack of
205 list counters, for nested lists.
207 def __init__(self, conv_textview, startiter):
208 xml.sax.handler.ContentHandler.__init__(self)
209 self.textbuf = conv_textview.tv.get_buffer()
210 self.textview = conv_textview.tv
211 self.iter = startiter
212 self.conv_textview = conv_textview
213 self.text = ''
214 self.starting=True
215 self.preserve = False
216 self.styles = [] # a gtk.TextTag or None, for each span level
217 self.list_counters = [] # stack (top at head) of list
218 # counters, or None for unordered list
220 def _parse_style_color(self, tag, value):
221 color = _parse_css_color(value)
222 tag.set_property('foreground-gdk', color)
224 def _parse_style_background_color(self, tag, value):
225 color = _parse_css_color(value)
226 tag.set_property('background-gdk', color)
227 tag.set_property('paragraph-background-gdk', color)
230 def _get_current_attributes(self):
231 attrs = self.textview.get_default_attributes()
232 self.iter.backward_char()
233 self.iter.get_attributes(attrs)
234 self.iter.forward_char()
235 return attrs
237 def __parse_length_frac_size_allocate(self, textview, allocation,
238 frac, callback, args):
239 callback(allocation.width*frac, *args)
241 def _parse_length(self, value, font_relative, block_relative, minl, maxl, callback, *args):
243 Parse/calc length, converting to pixels, calls callback(length, *args)
244 when the length is first computed or changes
246 if value.endswith('%'):
247 val = float(value[:-1])
248 sign = cmp(val, 0)
249 # limits: 1% to 500%
250 val = sign*max(1, min(abs(val), 500))
251 frac = val/100
252 if font_relative:
253 attrs = self._get_current_attributes()
254 font_size = attrs.font.get_size() / pango.SCALE
255 callback(frac*display_resolution*font_size, *args)
256 elif block_relative:
257 # CSS says 'Percentage values: refer to width of the closest
258 # block-level ancestor'
259 # This is difficult/impossible to implement, so we use
260 # textview width instead; a reasonable approximation..
261 alloc = self.textview.get_allocation()
262 self.__parse_length_frac_size_allocate(self.textview, alloc,
263 frac, callback, args)
264 self.textview.connect('size-allocate',
265 self.__parse_length_frac_size_allocate,
266 frac, callback, args)
267 else:
268 callback(frac, *args)
269 return
271 def get_val():
272 val = float(value[:-2])
273 sign = cmp(val, 0)
274 # validate length
275 return sign*max(minl, min(abs(val*display_resolution), maxl))
276 if value.endswith('pt'): # points
277 callback(get_val()*display_resolution, *args)
279 elif value.endswith('em'): # ems, the width of the element's font
280 attrs = self._get_current_attributes()
281 font_size = attrs.font.get_size() / pango.SCALE
282 callback(get_val()*display_resolution*font_size, *args)
284 elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
285 # FIXME: figure out how to calculate this correctly
286 # for now 'em' size is used as approximation
287 attrs = self._get_current_attributes()
288 font_size = attrs.font.get_size() / pango.SCALE
289 callback(get_val()*display_resolution*font_size, *args)
291 elif value.endswith('px'): # pixels
292 callback(get_val(), *args)
294 else:
295 try:
296 # TODO: isn't "no units" interpreted as pixels?
297 val = int(value)
298 sign = cmp(val, 0)
299 # validate length
300 val = sign*max(minl, min(abs(val), maxl))
301 callback(val, *args)
302 except Exception:
303 warnings.warn('Unable to parse length value "%s"' % value)
305 def __parse_font_size_cb(length, tag):
306 tag.set_property('size-points', length/display_resolution)
307 __parse_font_size_cb = staticmethod(__parse_font_size_cb)
309 def _parse_style_display(self, tag, value):
310 if value == 'none':
311 tag.set_property('invisible', 'true')
312 # FIXME: display: block, inline
314 def _parse_style_font_size(self, tag, value):
315 try:
316 scale = {
317 'xx-small': pango.SCALE_XX_SMALL,
318 'x-small': pango.SCALE_X_SMALL,
319 'small': pango.SCALE_SMALL,
320 'medium': pango.SCALE_MEDIUM,
321 'large': pango.SCALE_LARGE,
322 'x-large': pango.SCALE_X_LARGE,
323 'xx-large': pango.SCALE_XX_LARGE,
324 } [value]
325 except KeyError:
326 pass
327 else:
328 attrs = self._get_current_attributes()
329 tag.set_property('scale', scale / attrs.font_scale)
330 return
331 if value == 'smaller':
332 tag.set_property('scale', pango.SCALE_SMALL)
333 return
334 if value == 'larger':
335 tag.set_property('scale', pango.SCALE_LARGE)
336 return
337 # font relative (5 ~ 4pt, 110 ~ 72pt)
338 self._parse_length(value, True, False, 5, 110, self.__parse_font_size_cb, tag)
340 def _parse_style_font_style(self, tag, value):
341 try:
342 style = {
343 'normal': pango.STYLE_NORMAL,
344 'italic': pango.STYLE_ITALIC,
345 'oblique': pango.STYLE_OBLIQUE,
346 } [value]
347 except KeyError:
348 warnings.warn('unknown font-style %s' % value)
349 else:
350 tag.set_property('style', style)
352 def __frac_length_tag_cb(self, length, tag, propname):
353 styles = self._get_style_tags()
354 if styles:
355 length += styles[-1].get_property(propname)
356 tag.set_property(propname, length)
357 #__frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
359 def _parse_style_margin_left(self, tag, value):
360 # block relative
361 self._parse_length(value, False, True, 1, 1000, self.__frac_length_tag_cb,
362 tag, 'left-margin')
364 def _parse_style_margin_right(self, tag, value):
365 # block relative
366 self._parse_length(value, False, True, 1, 1000, self.__frac_length_tag_cb,
367 tag, 'right-margin')
369 def _parse_style_font_weight(self, tag, value):
370 # TODO: missing 'bolder' and 'lighter'
371 try:
372 weight = {
373 '100': pango.WEIGHT_ULTRALIGHT,
374 '200': pango.WEIGHT_ULTRALIGHT,
375 '300': pango.WEIGHT_LIGHT,
376 '400': pango.WEIGHT_NORMAL,
377 '500': pango.WEIGHT_NORMAL,
378 '600': pango.WEIGHT_BOLD,
379 '700': pango.WEIGHT_BOLD,
380 '800': pango.WEIGHT_ULTRABOLD,
381 '900': pango.WEIGHT_HEAVY,
382 'normal': pango.WEIGHT_NORMAL,
383 'bold': pango.WEIGHT_BOLD,
384 } [value]
385 except KeyError:
386 warnings.warn('unknown font-style %s' % value)
387 else:
388 tag.set_property('weight', weight)
390 def _parse_style_font_family(self, tag, value):
391 tag.set_property('family', value)
393 def _parse_style_text_align(self, tag, value):
394 try:
395 align = {
396 'left': gtk.JUSTIFY_LEFT,
397 'right': gtk.JUSTIFY_RIGHT,
398 'center': gtk.JUSTIFY_CENTER,
399 'justify': gtk.JUSTIFY_FILL,
400 } [value]
401 except KeyError:
402 warnings.warn('Invalid text-align:%s requested' % value)
403 else:
404 tag.set_property('justification', align)
406 def _parse_style_text_decoration(self, tag, value):
407 values = value.split(' ')
408 if 'none' in values:
409 tag.set_property('underline', pango.UNDERLINE_NONE)
410 tag.set_property('strikethrough', False)
411 if 'underline' in values:
412 tag.set_property('underline', pango.UNDERLINE_SINGLE)
413 else:
414 tag.set_property('underline', pango.UNDERLINE_NONE)
415 if 'line-through' in values:
416 tag.set_property('strikethrough', True)
417 else:
418 tag.set_property('strikethrough', False)
419 if 'blink' in values:
420 warnings.warn('text-decoration:blink not implemented')
421 if 'overline' in values:
422 warnings.warn('text-decoration:overline not implemented')
424 def _parse_style_white_space(self, tag, value):
425 if value == 'pre':
426 tag.set_property('wrap_mode', gtk.WRAP_NONE)
427 elif value == 'normal':
428 tag.set_property('wrap_mode', gtk.WRAP_WORD)
429 elif value == 'nowrap':
430 tag.set_property('wrap_mode', gtk.WRAP_NONE)
432 def __length_tag_cb(self, value, tag, propname):
433 try:
434 tag.set_property(propname, value)
435 except Exception:
436 gajim.log.warn( "Error with prop: " + propname + " for tag: " + str(tag))
439 def _parse_style_width(self, tag, value):
440 if value == 'auto':
441 return
442 self._parse_length(value, False, False, 1, 1000, self.__length_tag_cb,
443 tag, "width")
444 def _parse_style_height(self, tag, value):
445 if value == 'auto':
446 return
447 self._parse_length(value, False, False, 1, 1000, self.__length_tag_cb,
448 tag, "height")
451 # build a dictionary mapping styles to methods, for greater speed
452 __style_methods = dict()
453 for style in ('background-color', 'color', 'font-family', 'font-size',
454 'font-style', 'font-weight', 'margin-left', 'margin-right',
455 'text-align', 'text-decoration', 'white-space', 'display',
456 'width', 'height' ):
457 try:
458 method = locals()['_parse_style_%s' % style.replace('-', '_')]
459 except KeyError:
460 warnings.warn('Style attribute "%s" not yet implemented' % style)
461 else:
462 __style_methods[style] = method
463 del style
464 # --
466 def _get_style_tags(self):
467 return [tag for tag in self.styles if tag is not None]
469 def _create_url(self, href, title, type_, id_):
470 '''Process a url tag.
472 tag = self.textbuf.create_tag(id_)
473 if href and href[0] != '#':
474 tag.href = href
475 tag.type_ = type_ # to be used by the URL handler
476 tag.connect('event', self.textview.html_hyperlink_handler, 'url', href)
477 tag.set_property('foreground', gajim.config.get('urlmsgcolor'))
478 tag.set_property('underline', pango.UNDERLINE_SINGLE)
479 tag.is_anchor = True
480 if title:
481 tag.title = title
482 return tag
484 def _process_img(self, attrs):
485 '''Process a img tag.
487 mem = ''
488 try:
489 # Wait maximum 1s for connection
490 socket.setdefaulttimeout(1)
491 try:
492 req = urllib2.Request(attrs['src'])
493 req.add_header('User-Agent', 'Gajim ' + gajim.version)
494 f = urllib2.urlopen(req)
495 except Exception, ex:
496 gajim.log.debug('Error loading image %s ' % attrs['src'] + str(ex))
497 pixbuf = None
498 alt = attrs.get('alt', 'Broken image')
499 else:
500 # Wait 0.1s between each byte
501 try:
502 f.fp._sock.fp._sock.settimeout(0.5)
503 except Exception:
504 pass
505 # Max image size = 2 MB (to try to prevent DoS)
506 deadline = time.time() + 3
507 while True:
508 if time.time() > deadline:
509 gajim.log.debug(str('Timeout loading image %s ' % \
510 attrs['src'] + ex))
511 mem = ''
512 alt = attrs.get('alt', '')
513 if alt:
514 alt += '\n'
515 alt += _('Timeout loading image')
516 break
517 try:
518 temp = f.read(100)
519 except socket.timeout, ex:
520 gajim.log.debug('Timeout loading image %s ' % attrs['src'] + \
521 str(ex))
522 alt = attrs.get('alt', '')
523 if alt:
524 alt += '\n'
525 alt += _('Timeout loading image')
526 break
527 if temp:
528 mem += temp
529 else:
530 break
531 if len(mem) > 2*1024*1024:
532 alt = attrs.get('alt', '')
533 if alt:
534 alt += '\n'
535 alt += _('Image is too big')
536 break
537 pixbuf = None
538 if mem:
539 # Caveat: GdkPixbuf is known not to be safe to load
540 # images from network... this program is now potentially
541 # hackable ;)
542 loader = gtk.gdk.PixbufLoader()
543 dims = [0, 0]
544 def height_cb(length):
545 dims[1] = length
546 def width_cb(length):
547 dims[0] = length
548 # process width and height attributes
549 w = attrs.get('width')
550 h = attrs.get('height')
551 # override with width and height styles
552 for attr, val in style_iter(attrs.get('style', '')):
553 if attr == 'width':
554 w = val
555 elif attr == 'height':
556 h = val
557 if w:
558 self._parse_length(w, False, False, 1, 1000, width_cb)
559 if h:
560 self._parse_length(h, False, False, 1, 1000, height_cb)
561 def set_size(pixbuf, w, h, dims):
563 FIXME: Floats should be relative to the whole textview, and
564 resize with it. This needs new pifbufs for every resize,
565 gtk.gdk.Pixbuf.scale_simple or similar.
567 if isinstance(dims[0], float):
568 dims[0] = int(dims[0]*w)
569 elif not dims[0]:
570 dims[0] = w
571 if isinstance(dims[1], float):
572 dims[1] = int(dims[1]*h)
573 if not dims[1]:
574 dims[1] = h
575 loader.set_size(*dims)
576 if w or h:
577 loader.connect('size-prepared', set_size, dims)
578 loader.write(mem)
579 loader.close()
580 pixbuf = loader.get_pixbuf()
581 alt = attrs.get('alt', '')
582 if pixbuf is not None:
583 tags = self._get_style_tags()
584 if tags:
585 tmpmark = self.textbuf.create_mark(None, self.iter, True)
586 self.textbuf.insert_pixbuf(self.iter, pixbuf)
587 self.starting = False
588 if tags:
589 start = self.textbuf.get_iter_at_mark(tmpmark)
590 for tag in tags:
591 self.textbuf.apply_tag(tag, start, self.iter)
592 self.textbuf.delete_mark(tmpmark)
593 else:
594 self._insert_text('[IMG: %s]' % alt)
595 except Exception, ex:
596 gajim.log.error('Error loading image ' + str(ex))
597 pixbuf = None
598 alt = attrs.get('alt', 'Broken image')
599 try:
600 loader.close()
601 except Exception:
602 pass
603 return pixbuf
605 def _begin_span(self, style, tag=None, id_=None):
606 if style is None:
607 self.styles.append(tag)
608 return None
609 if tag is None:
610 if id_:
611 tag = self.textbuf.create_tag(id_)
612 else:
613 tag = self.textbuf.create_tag() # we create anonymous tag
614 for attr, val in style_iter(style):
615 attr = attr.lower()
616 val = val
617 try:
618 method = self.__style_methods[attr]
619 except KeyError:
620 warnings.warn('Style attribute "%s" requested '
621 'but not yet implemented' % attr)
622 else:
623 method(self, tag, val)
624 self.styles.append(tag)
626 def _end_span(self):
627 self.styles.pop()
629 def _jump_line(self):
630 self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
631 self.starting = True
633 def _insert_text(self, text):
634 if self.starting and text != '\n':
635 self.starting = (text[-1] == '\n')
636 tags = self._get_style_tags()
637 if tags:
638 self.textbuf.insert_with_tags(self.iter, text, *tags)
639 else:
640 self.textbuf.insert(self.iter, text)
642 def _starts_line(self):
643 return self.starting or self.iter.starts_line()
645 def _flush_text(self):
646 if not self.text: return
647 text, self.text = self.text, ''
648 if not self.preserve:
649 text = text.replace('\n', ' ')
650 self.handle_specials(whitespace_rx.sub(' ', text))
651 else:
652 self._insert_text(text.strip('\n'))
654 def _anchor_event(self, tag, textview, event, iter_, href, type_):
655 if event.type == gtk.gdk.BUTTON_PRESS:
656 self.textview.emit('url-clicked', href, type_)
657 return True
658 return False
660 def handle_specials(self, text):
661 self.iter = self.conv_textview.detect_and_print_special_text(text, self._get_style_tags())
663 def characters(self, content):
664 if self.preserve:
665 self.text += content
666 return
667 if allwhitespace_rx.match(content) is not None and self._starts_line():
668 self.text += ' '
669 return
670 self.text += content
671 self.starting = False
674 def startElement(self, name, attrs):
675 self._flush_text()
676 klass = [i for i in attrs.get('class', ' ').split(' ') if i]
677 style = ''
678 #Add styles defined for classes
679 for k in klass:
680 if k in classes:
681 style += classes[k]
683 tag = None
684 #FIXME: if we want to use id, it needs to be unique across
685 # the whole textview, so we need to add something like the
686 # message-id to it.
687 #id_ = attrs.get('id',None)
688 id_ = None
689 if name == 'a':
690 #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
691 href = attrs.get('href', None)
692 if not href:
693 href = attrs.get('HREF', None)
694 # Gaim sends HREF instead of href
695 title = attrs.get('title', attrs.get('rel', href))
696 type_ = attrs.get('type', None)
697 tag = self._create_url(href, title, type_, id_)
698 elif name == 'blockquote':
699 cite = attrs.get('cite', None)
700 if cite:
701 tag = self.textbuf.create_tag(id_)
702 tag.title = title
703 tag.is_anchor = True
704 elif name in LIST_ELEMS:
705 style += ';margin-left: 2em'
706 elif name == 'img':
707 tag = self._process_img(attrs)
708 if name in element_styles:
709 style += element_styles[name]
710 # so that explicit styles override implicit ones,
711 # we add the attribute last
712 style += ";"+attrs.get('style', '')
713 if style == '':
714 style = None
715 self._begin_span(style, tag, id_)
717 if name == 'br':
718 pass # handled in endElement
719 elif name == 'hr':
720 pass # handled in endElement
721 elif name in BLOCK:
722 if not self._starts_line():
723 self._jump_line()
724 if name == 'pre':
725 self.preserve = True
726 elif name == 'span':
727 pass
728 elif name in ('dl', 'ul'):
729 if not self._starts_line():
730 self._jump_line()
731 self.list_counters.append(None)
732 elif name == 'ol':
733 if not self._starts_line():
734 self._jump_line()
735 self.list_counters.append(0)
736 elif name == 'li':
737 if self.list_counters[-1] is None:
738 li_head = unichr(0x2022)
739 else:
740 self.list_counters[-1] += 1
741 li_head = '%i.' % self.list_counters[-1]
742 self.text = ' '*len(self.list_counters)*4 + li_head + ' '
743 self._flush_text()
744 self.starting = True
745 elif name == 'dd':
746 self._jump_line()
747 elif name == 'dt':
748 if not self.starting:
749 self._jump_line()
750 elif name in ('a', 'img', 'body', 'html'):
751 pass
752 elif name in INLINE:
753 pass
754 else:
755 warnings.warn('Unhandled element "%s"' % name)
757 def endElement(self, name):
758 endPreserving = False
759 newLine = False
760 if name == 'br':
761 newLine = True
762 elif name == 'hr':
763 #FIXME: plenty of unused attributes (width, height,...) :)
764 self._jump_line()
765 try:
766 self.textbuf.insert_pixbuf(self.iter, self.textview.focus_out_line_pixbuf)
767 #self._insert_text(u'\u2550'*40)
768 self._jump_line()
769 except Exception, e:
770 gajim.log.debug(str('Error in hr'+e))
771 elif name in LIST_ELEMS:
772 self.list_counters.pop()
773 elif name == 'li':
774 newLine = True
775 elif name == 'img':
776 pass
777 elif name == 'body' or name == 'html':
778 pass
779 elif name == 'a':
780 pass
781 elif name in INLINE:
782 pass
783 elif name in ('dd', 'dt', ):
784 pass
785 elif name in BLOCK:
786 if name == 'pre':
787 endPreserving = True
788 else:
789 warnings.warn("Unhandled element '%s'" % name)
790 self._flush_text()
791 if endPreserving:
792 self.preserve = False
793 if newLine:
794 self._jump_line()
795 self._end_span()
796 #if not self._starts_line():
797 # self.text = ' '
799 class HtmlTextView(gtk.TextView):
801 def __init__(self):
802 gobject.GObject.__init__(self)
803 self.set_wrap_mode(gtk.WRAP_CHAR)
804 self.set_editable(False)
805 self._changed_cursor = False
806 self.connect('destroy', self.__destroy_event)
807 self.connect('motion-notify-event', self.__motion_notify_event)
808 self.connect('leave-notify-event', self.__leave_event)
809 self.connect('enter-notify-event', self.__motion_notify_event)
810 self.connect('realize', self.on_html_text_view_realized)
811 self.connect('unrealize', self.on_html_text_view_unrealized)
812 self.connect('copy-clipboard', self.on_html_text_view_copy_clipboard)
813 self.get_buffer().connect_after('mark-set', self.on_text_buffer_mark_set)
814 self.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL)
815 self.tooltip = tooltips.BaseTooltip()
816 self.config = gajim.config
817 self.interface = gajim.interface
818 # end big hack
820 def __destroy_event(self, widget):
821 if self.tooltip.timeout != 0:
822 self.tooltip.hide_tooltip()
824 def __leave_event(self, widget, event):
825 if self._changed_cursor:
826 window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
827 window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
828 self._changed_cursor = False
830 def show_tooltip(self, tag):
831 if not self.tooltip.win:
832 # check if the current pointer is still over the line
833 x, y, _ = self.window.get_pointer()
834 x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
835 tags = self.get_iter_at_location(x, y).get_tags()
836 is_over_anchor = False
837 for tag_ in tags:
838 if getattr(tag_, 'is_anchor', False):
839 is_over_anchor = True
840 break
841 if not is_over_anchor:
842 return
843 text = getattr(tag, 'title', False)
844 if text:
845 pointer = self.get_pointer()
846 position = self.window.get_origin()
847 self.tooltip.show_tooltip(text, 8, position[1] + pointer[1])
849 def __motion_notify_event(self, widget, event):
850 x, y, _ = widget.window.get_pointer()
851 x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
852 tags = widget.get_iter_at_location(x, y).get_tags()
853 anchor_tags = [tag for tag in tags if getattr(tag, 'is_anchor', False)]
854 if self.tooltip.timeout != 0:
855 # Check if we should hide the line tooltip
856 if not anchor_tags:
857 self.tooltip.hide_tooltip()
858 if not self._changed_cursor and anchor_tags:
859 window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
860 window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
861 self._changed_cursor = True
862 self.tooltip.timeout = gobject.timeout_add(500, self.show_tooltip, anchor_tags[0])
863 elif self._changed_cursor and not anchor_tags:
864 window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
865 window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
866 self._changed_cursor = False
867 return False
869 def display_html(self, html, conv_textview):
870 buffer_ = self.get_buffer()
871 eob = buffer_.get_end_iter()
872 ## this works too if libxml2 is not available
873 # parser = xml.sax.make_parser(['drv_libxml2'])
874 # parser.setFeature(xml.sax.handler.feature_validation, True)
875 parser = xml.sax.make_parser()
876 parser.setContentHandler(HtmlHandler(conv_textview, eob))
877 parser.parse(StringIO(html))
879 # too much space after :)
880 #if not eob.starts_line():
881 # buffer_.insert(eob, '\n')
883 def on_html_text_view_copy_clipboard(self, unused_data):
884 clipboard = self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD)
885 clipboard.set_text(self.get_selected_text())
886 self.emit_stop_by_name('copy-clipboard')
888 def on_html_text_view_realized(self, unused_data):
889 self.get_buffer().remove_selection_clipboard(self.get_clipboard(gtk.gdk.SELECTION_PRIMARY))
891 def on_html_text_view_unrealized(self, unused_data):
892 self.get_buffer().add_selection_clipboard(self.get_clipboard(gtk.gdk.SELECTION_PRIMARY))
894 def on_text_buffer_mark_set(self, location, mark, unused_data):
895 bounds = self.get_buffer().get_selection_bounds()
896 if bounds:
897 # textview can be hidden while we add a new line in it.
898 if self.has_screen():
899 clipboard = self.get_clipboard(gtk.gdk.SELECTION_PRIMARY)
900 clipboard.set_text(self.get_selected_text())
902 def get_selected_text(self):
903 bounds = self.get_buffer().get_selection_bounds()
904 selection = ''
905 if bounds:
906 (search_iter, end) = bounds
908 while (search_iter.compare(end)):
909 character = search_iter.get_char()
910 if character == u'\ufffc':
911 anchor = search_iter.get_child_anchor()
912 if anchor:
913 text = anchor.get_data('plaintext')
914 if text:
915 selection+=text
916 else:
917 selection+=character
918 else:
919 selection+=character
920 search_iter.forward_char()
921 return selection
923 change_cursor = None
925 if __name__ == '__main__':
926 import os
928 from conversation_textview import ConversationTextview
929 import gajim as gaj
931 class log(object):
933 def debug(self, text):
934 print "debug:", text
935 def warn(self, text):
936 print "warn;", text
937 def error(self, text):
938 print "error;", text
940 gajim.log=log()
942 gaj.Interface()
944 htmlview = ConversationTextview(None)
946 path = gtkgui_helpers.get_icon_path('gajim-muc_separator')
947 # use this for hr
948 htmlview.tv.focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path)
950 tooltip = tooltips.BaseTooltip()
952 def on_textview_motion_notify_event(widget, event):
954 Change the cursor to a hand when we are over a mail or an url
956 global change_cursor
957 pointer_x, pointer_y = htmlview.tv.window.get_pointer()[0:2]
958 x, y = htmlview.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x,
959 pointer_y)
960 tags = htmlview.tv.get_iter_at_location(x, y).get_tags()
961 if change_cursor:
962 htmlview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
963 gtk.gdk.Cursor(gtk.gdk.XTERM))
964 change_cursor = None
965 tag_table = htmlview.tv.get_buffer().get_tag_table()
966 for tag in tags:
967 try:
968 if tag.is_anchor:
969 htmlview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
970 gtk.gdk.Cursor(gtk.gdk.HAND2))
971 change_cursor = tag
972 elif tag == tag_table.lookup('focus-out-line'):
973 over_line = True
974 except Exception:
975 pass
977 #if line_tooltip.timeout != 0:
978 # Check if we should hide the line tooltip
979 # if not over_line:
980 # line_tooltip.hide_tooltip()
981 #if over_line and not line_tooltip.win:
982 # line_tooltip.timeout = gobject.timeout_add(500,
983 # show_line_tooltip)
984 # htmlview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
985 # gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
986 # change_cursor = tag
988 htmlview.tv.connect('motion_notify_event', on_textview_motion_notify_event)
990 def handler(texttag, widget, event, iter_, kind, href):
991 if event.type == gtk.gdk.BUTTON_PRESS:
992 print href
994 htmlview.tv.html_hyperlink_handler = handler
996 htmlview.print_real_text(None, xhtml='<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n'
997 ' <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
998 ' <span style="font-size: 500%; font-family: serif">World</span>\n'
999 '</div>\n')
1000 htmlview.print_real_text(None, xhtml='<hr />')
1001 htmlview.print_real_text(None, xhtml='''<body xmlns='http://www.w3.org/1999/xhtml'><p xmlns='http://www.w3.org/1999/xhtml'>a:b<a href='http://google.com/' xmlns='http://www.w3.org/1999/xhtml'>Google</a></p><br/></body>''')
1002 htmlview.print_real_text(None, xhtml='''
1003 <body xmlns='http://www.w3.org/1999/xhtml'>
1004 <p style='font-size:large'>
1005 <span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
1006 I&apos;m <span style='color:green'>green</span>
1007 with <span style='font-weight: bold'>envy</span>!
1008 </p>
1009 </body>
1010 ''')
1011 htmlview.print_real_text(None, xhtml='<hr />')
1012 htmlview.print_real_text(None, xhtml='''
1013 <body xmlns='http://www.w3.org/1999/xhtml'>
1014 http://test.com/ testing links autolinkifying
1015 </body>
1016 ''')
1017 htmlview.print_real_text(None, xhtml='<hr />')
1018 htmlview.print_real_text(None, xhtml='''
1019 <body xmlns='http://www.w3.org/1999/xhtml'>
1020 <p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
1021 <p style='margin-left: 5px; margin-right: 2%'>
1022 &quot;A foolish consistency is the hobgoblin of little minds.&quot;
1023 </p>
1024 </body>
1025 ''')
1026 htmlview.print_real_text(None, xhtml='<hr />')
1027 htmlview.print_real_text(None, xhtml='''
1028 <body xmlns='http://www.w3.org/1999/xhtml'>
1029 <p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p>
1030 <p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg'
1031 alt='A License to Jabber'
1032 width='50%' height='50%'
1033 /></p>
1034 </body>
1035 ''')
1036 htmlview.print_real_text(None, xhtml='<hr />')
1037 htmlview.print_real_text(None, xhtml='''
1038 <body xmlns='http://www.w3.org/1999/xhtml'>
1039 <ul style='background-color:rgb(120,140,100)'>
1040 <li> One </li>
1041 <li> Two </li>
1042 <li> Three </li>
1043 </ul><hr /><pre style="background-color:rgb(120,120,120)">def fac(n):
1044 def faciter(n,acc):
1045 if n==0: return acc
1046 return faciter(n-1, acc*n)
1047 if n&lt;0: raise ValueError('Must be non-negative')
1048 return faciter(n,1)</pre>
1049 </body>
1050 ''')
1051 htmlview.print_real_text(None, xhtml='<hr />')
1052 htmlview.print_real_text(None, xhtml='''
1053 <body xmlns='http://www.w3.org/1999/xhtml'>
1054 <ol style='background-color:rgb(120,140,100)'>
1055 <li> One </li>
1056 <li> Two is nested: <ul style='background-color:rgb(200,200,100)'>
1057 <li> One </li>
1058 <li style='font-size:50%'> Two </li>
1059 <li style='font-size:200%'> Three </li>
1060 <li style='font-size:9999pt'> Four </li>
1061 </ul></li>
1062 <li> Three </li></ol>
1063 </body>
1064 ''')
1065 htmlview.tv.show()
1066 sw = gtk.ScrolledWindow()
1067 sw.set_property('hscrollbar-policy', gtk.POLICY_AUTOMATIC)
1068 sw.set_property('vscrollbar-policy', gtk.POLICY_AUTOMATIC)
1069 sw.set_property('border-width', 0)
1070 sw.add(htmlview.tv)
1071 sw.show()
1072 frame = gtk.Frame()
1073 frame.set_shadow_type(gtk.SHADOW_IN)
1074 frame.show()
1075 frame.add(sw)
1076 w = gtk.Window()
1077 w.add(frame)
1078 w.set_default_size(400, 300)
1079 w.show_all()
1080 w.connect('destroy', lambda w: gtk.main_quit())
1081 gtk.main()