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/>.
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
41 import xml
.sax
, xml
.sax
.handler
43 from cStringIO
import StringIO
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)
55 from common
import gajim
56 from gtkgui_helpers
import get_icon_pixmap
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
73 #'system-message':';display: none',
74 'problematic': ';color: red',
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']
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:
113 # addr, blockquote, pre
117 # h1, h2, h3, h4, h5, h6
122 # abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
127 # - List (ul, ol, dl)
131 # Therefore XHTML-IM uses the following content models:
134 # Block-like elements, e.g., paragraphs
136 # Any block or inline elements
138 # Character-level elements
139 # InlineNoAnchor.class
144 # XHTML-IM also uses the following Attribute Groups:
157 # ( pres = h1 | h2 | h3 | h4 | h5 | h6 )
158 # Block ( phrasal = address | blockquote | pre )
159 # NOT ( presentational = hr )
160 # ( structural = div | p )
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,
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
:
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
)
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
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()
239 def __parse_length_frac_size_allocate(self
, textview
, allocation
, frac
,
241 callback(allocation
.width
*frac
, *args
)
243 def _parse_length(self
, value
, font_relative
, block_relative
, minl
, maxl
,
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])
253 val
= sign
*max(1, min(abs(val
), 500))
256 attrs
= self
._get
_current
_attributes
()
257 font_size
= attrs
.font
.get_size() / pango
.SCALE
258 callback(frac
*display_resolution
*font_size
, *args
)
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
)
271 callback(frac
, *args
)
275 val
= float(value
[:-2])
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
)
299 # TODO: isn't "no units" interpreted as pixels?
303 val
= sign
*max(minl
, min(abs(val
), maxl
))
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
):
314 tag
.set_property('invisible', 'true')
315 # FIXME: display: block, inline
317 def _parse_style_font_size(self
, tag
, value
):
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
,
331 attrs
= self
._get
_current
_attributes
()
332 tag
.set_property('scale', scale
/ attrs
.font_scale
)
334 if value
== 'smaller':
335 tag
.set_property('scale', pango
.SCALE_SMALL
)
337 if value
== 'larger':
338 tag
.set_property('scale', pango
.SCALE_LARGE
)
340 # font relative (5 ~ 4pt, 110 ~ 72pt)
341 self
._parse
_length
(value
, True, False, 5, 110,self
.__parse
_font
_size
_cb
,
344 def _parse_style_font_style(self
, tag
, value
):
347 'normal': pango
.STYLE_NORMAL
,
348 'italic': pango
.STYLE_ITALIC
,
349 'oblique': pango
.STYLE_OBLIQUE
,
352 log
.warning('unknown font-style %s' % value
)
354 tag
.set_property('style', style
)
356 def __frac_length_tag_cb(self
, length
, tag
, propname
):
357 styles
= self
._get
_style
_tags
()
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
):
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
):
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'
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
,
390 log
.warning('unknown font-style %s' % value
)
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
):
400 'left': gtk
.JUSTIFY_LEFT
,
401 'right': gtk
.JUSTIFY_RIGHT
,
402 'center': gtk
.JUSTIFY_CENTER
,
403 'justify': gtk
.JUSTIFY_FILL
,
406 log
.warning('Invalid text-align:%s requested' % value
)
408 tag
.set_property('justification', align
)
410 def _parse_style_text_decoration(self
, tag
, value
):
411 values
= value
.split(' ')
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
)
418 tag
.set_property('underline', pango
.UNDERLINE_NONE
)
419 if 'line-through' in values
:
420 tag
.set_property('strikethrough', True)
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
):
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
):
438 tag
.set_property(propname
, value
)
440 log
.warning( "Error with prop: " + propname
+ " for tag: " + str(tag
))
443 def _parse_style_width(self
, tag
, value
):
446 self
._parse
_length
(value
, False, False, 1, 1000, self
.__length
_tag
_cb
,
448 def _parse_style_height(self
, tag
, value
):
451 self
._parse
_length
(value
, False, False, 1, 1000, self
.__length
_tag
_cb
,
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',
462 method
= locals()['_parse_style_%s' % style
.replace('-', '_')]
464 log
.warning('Style attribute "%s" not yet implemented' % style
)
466 __style_methods
[style
] = method
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] != '#':
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
)
488 def _get_img(self
, attrs
):
489 '''Download an image. This function is launched in a separate thread.
492 # Wait maximum 5s for connection
493 socket
.setdefaulttimeout(5)
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
))
501 alt
= attrs
.get('alt', 'Broken image')
503 # Wait 0.5s between each byte
505 f
.fp
._sock
.fp
._sock
.settimeout(0.5)
508 # Max image size = 2 MB (to try to prevent DoS)
509 deadline
= time
.time() + 3
511 if time
.time() > deadline
:
512 log
.debug(str('Timeout loading image %s ' % \
515 alt
= attrs
.get('alt', '')
518 alt
+= _('Timeout loading image')
522 except socket
.timeout
, ex
:
523 log
.debug('Timeout loading image %s ' % \
524 attrs
['src'] + str(ex
))
525 alt
= attrs
.get('alt', '')
528 alt
+= _('Timeout loading image')
534 if len(mem
) > 2*1024*1024:
535 alt
= attrs
.get('alt', '')
538 alt
+= _('Image is too big')
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.
556 if attrs
['src'].startswith('data:image/'):
557 # The "data" URL scheme http://tools.ietf.org/html/rfc2397
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
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', '')
572 pixbuf
= get_icon_pixmap('gajim-receipt_missing')
574 # Caveat: GdkPixbuf is known not to be safe to load
575 # images from network... this program is now potentially
577 loader
= gtk
.gdk
.PixbufLoader()
579 def height_cb(length
):
581 def width_cb(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', '')):
590 elif attr
== 'height':
593 self
._parse
_length
(w
, False, False, 1, 1000, width_cb
)
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
)
606 if isinstance(dims
[1], float):
607 dims
[1] = int(dims
[1]*h
)
610 loader
.set_size(*dims
)
612 loader
.connect('size-prepared', set_size
, dims
)
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
()
627 tmpmark
= self
.textbuf
.create_mark(None, working_iter
, True)
628 self
.textbuf
.insert_pixbuf(working_iter
, pixbuf
)
629 self
.starting
= False
631 start
= self
.textbuf
.get_iter_at_mark(tmpmark
)
633 self
.textbuf
.apply_tag(tag
, start
, working_iter
)
634 self
.textbuf
.delete_mark(tmpmark
)
636 self
._insert
_text
('[IMG: %s]' % alt
, working_iter
)
637 except Exception, ex
:
638 log
.error('Error loading image ' + str(ex
))
640 alt
= attrs
.get('alt', 'Broken image')
647 def _begin_span(self
, style
, tag
=None, id_
=None):
649 self
.styles
.append(tag
)
653 tag
= self
.textbuf
.create_tag(id_
)
655 tag
= self
.textbuf
.create_tag() # we create anonymous tag
656 for attr
, val
in style_iter(style
):
660 method
= self
.__style
_methods
[attr
]
662 log
.warning('Style attribute "%s" requested '
663 'but not yet implemented' % attr
)
665 method(self
, tag
, val
)
666 self
.styles
.append(tag
)
671 def _jump_line(self
):
672 self
.textbuf
.insert_with_tags_by_name(self
.iter, '\n', 'eol')
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
()
682 self
.textbuf
.insert_with_tags(working_iter
, text
, *tags
)
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
))
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_
)
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
):
712 if allwhitespace_rx
.match(content
) is not None and self
._starts
_line
():
715 self
.starting
= False
718 def startElement(self
, name
, attrs
):
720 klass
= [i
for i
in attrs
.get('class', ' ').split(' ') if i
]
722 #Add styles defined for classes
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
731 #id_ = attrs.get('id',None)
734 #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
735 href
= attrs
.get('href', None)
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)
745 tag
= self
.textbuf
.create_tag(id_
)
748 elif name
in LIST_ELEMS
:
749 style
+= ';margin-left: 2em'
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', '')
759 self
._begin
_span
(style
, tag
, id_
)
762 pass # handled in endElement
764 pass # handled in endElement
766 if not self
._starts
_line
():
772 elif name
in ('dl', 'ul'):
773 if not self
._starts
_line
():
775 self
.list_counters
.append(None)
777 if not self
._starts
_line
():
779 self
.list_counters
.append(0)
781 if self
.list_counters
[-1] is None:
782 li_head
= unichr(0x2022)
784 self
.list_counters
[-1] += 1
785 li_head
= '%i.' % self
.list_counters
[-1]
786 self
.text
= ' '*len(self
.list_counters
)*4 + li_head
+ ' '
792 if not self
.starting
:
794 elif name
in ('a', 'img', 'body', 'html'):
799 log
.warning('Unhandled element "%s"' % name
)
801 def endElement(self
, name
):
802 endPreserving
= False
807 #FIXME: plenty of unused attributes (width, height,...) :)
810 self
.textbuf
.insert_pixbuf(self
.iter,
811 self
.textview
.focus_out_line_pixbuf
)
812 #self._insert_text(u'\u2550'*40)
815 log
.debug(str('Error in hr'+e
))
816 elif name
in LIST_ELEMS
:
817 self
.list_counters
.pop()
822 elif name
== 'body' or name
== 'html':
828 elif name
in ('dd', 'dt', ):
834 log
.warning("Unhandled element '%s'" % name
)
837 self
.preserve
= False
841 #if not self._starts_line():
844 class HtmlTextView(gtk
.TextView
):
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
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
883 if getattr(tag_
, 'is_anchor', False):
884 is_over_anchor
= True
886 if not is_over_anchor
:
888 text
= getattr(tag
, 'title', False)
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
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
,
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
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()
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()
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()
961 text
= anchor
.get_data('plaintext')
968 search_iter
.forward_char()
973 if __name__
== '__main__':
976 from conversation_textview
import ConversationTextview
979 log
= logging
.getLogger()
982 htmlview
= ConversationTextview(None)
984 path
= gtkgui_helpers
.get_icon_path('gajim-muc_separator')
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
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()
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()
1007 htmlview
.tv
.get_window(gtk
.TEXT_WINDOW_TEXT
).set_cursor(
1008 gtk
.gdk
.Cursor(gtk
.gdk
.HAND2
))
1010 elif tag
== tag_table
.lookup('focus-out-line'):
1015 #if line_tooltip.timeout != 0:
1016 # Check if we should hide the line tooltip
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
:
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'
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
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'm <span style='color:green'>green</span>
1053 with <span style='font-weight: bold'>envy</span>!
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
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 "A foolish consistency is the hobgoblin of little minds."
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>?
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%'/>
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)'>
1092 </ul><hr /><pre style="background-color:rgb(120,120,120)">def fac(n):
1095 return faciter(n-1, acc*n)
1096 if n<0: raise ValueError('Must be non-negative')
1097 return faciter(n,1)</pre>
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)'>
1105 <li> Two is nested: <ul style='background-color:rgb(200,200,100)'>
1107 <li style='font-size:50%'> Two </li>
1108 <li style='font-size:200%'> Three </li>
1109 <li style='font-size:9999pt'> Four </li>
1111 <li> Three </li></ol>
1114 htmlview
.print_real_text(None, xhtml
='<hr />')
1115 htmlview
.print_real_text(None, xhtml
='''
1116 <body xmlns='http://www.w3.org/1999/xhtml'>
1119 <a href='xmpp:example@example.org'>xmpp link</a>
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>
1126 <p>this cite - bla bla bla, smile- :-) ...</p>
1136 htmlview
.print_real_text(None, xhtml
='<hr />')
1137 htmlview
.print_real_text(None, xhtml
='''
1138 <body xmlns='http://www.w3.org/1999/xhtml'>
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'/>
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)
1157 frame
.set_shadow_type(gtk
.SHADOW_IN
)
1162 w
.set_default_size(400, 300)
1164 w
.connect('destroy', lambda w
: gtk
.main_quit())