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://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
41 import xml
.sax
, xml
.sax
.handler
44 from cStringIO
import StringIO
50 if __name__
== '__main__':
51 from common
import i18n
52 import common
.configpaths
53 common
.configpaths
.gajimpaths
.init(None)
54 from common
import gajim
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
70 #'system-message':';display: none',
71 'problematic': ';color: red',
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']
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:
110 # addr, blockquote, pre
114 # h1, h2, h3, h4, h5, h6
119 # abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
124 # - List (ul, ol, dl)
128 # Therefore XHTML-IM uses the following content models:
131 # Block-like elements, e.g., paragraphs
133 # Any block or inline elements
135 # Character-level elements
136 # InlineNoAnchor.class
141 # XHTML-IM also uses the following Attribute Groups:
154 # ( pres = h1 | h2 | h3 | h4 | h5 | h6 )
155 # Block ( phrasal = address | blockquote | pre )
156 # NOT ( presentational = hr )
157 # ( structural = div | p )
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,
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
:
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
)
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
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()
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])
250 val
= sign
*max(1, min(abs(val
), 500))
253 attrs
= self
._get
_current
_attributes
()
254 font_size
= attrs
.font
.get_size() / pango
.SCALE
255 callback(frac
*display_resolution
*font_size
, *args
)
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
)
268 callback(frac
, *args
)
272 val
= float(value
[:-2])
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
)
296 # TODO: isn't "no units" interpreted as pixels?
300 val
= sign
*max(minl
, min(abs(val
), maxl
))
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
):
311 tag
.set_property('invisible', 'true')
312 # FIXME: display: block, inline
314 def _parse_style_font_size(self
, tag
, value
):
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
,
328 attrs
= self
._get
_current
_attributes
()
329 tag
.set_property('scale', scale
/ attrs
.font_scale
)
331 if value
== 'smaller':
332 tag
.set_property('scale', pango
.SCALE_SMALL
)
334 if value
== 'larger':
335 tag
.set_property('scale', pango
.SCALE_LARGE
)
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
):
343 'normal': pango
.STYLE_NORMAL
,
344 'italic': pango
.STYLE_ITALIC
,
345 'oblique': pango
.STYLE_OBLIQUE
,
348 warnings
.warn('unknown font-style %s' % value
)
350 tag
.set_property('style', style
)
352 def __frac_length_tag_cb(self
, length
, tag
, propname
):
353 styles
= self
._get
_style
_tags
()
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
):
361 self
._parse
_length
(value
, False, True, 1, 1000, self
.__frac
_length
_tag
_cb
,
364 def _parse_style_margin_right(self
, tag
, value
):
366 self
._parse
_length
(value
, False, True, 1, 1000, self
.__frac
_length
_tag
_cb
,
369 def _parse_style_font_weight(self
, tag
, value
):
370 # TODO: missing 'bolder' and 'lighter'
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
,
386 warnings
.warn('unknown font-style %s' % value
)
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
):
396 'left': gtk
.JUSTIFY_LEFT
,
397 'right': gtk
.JUSTIFY_RIGHT
,
398 'center': gtk
.JUSTIFY_CENTER
,
399 'justify': gtk
.JUSTIFY_FILL
,
402 warnings
.warn('Invalid text-align:%s requested' % value
)
404 tag
.set_property('justification', align
)
406 def _parse_style_text_decoration(self
, tag
, value
):
407 values
= value
.split(' ')
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
)
414 tag
.set_property('underline', pango
.UNDERLINE_NONE
)
415 if 'line-through' in values
:
416 tag
.set_property('strikethrough', True)
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
):
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
):
434 tag
.set_property(propname
, value
)
436 gajim
.log
.warn( "Error with prop: " + propname
+ " for tag: " + str(tag
))
439 def _parse_style_width(self
, tag
, value
):
442 self
._parse
_length
(value
, False, False, 1, 1000, self
.__length
_tag
_cb
,
444 def _parse_style_height(self
, tag
, value
):
447 self
._parse
_length
(value
, False, False, 1, 1000, self
.__length
_tag
_cb
,
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',
458 method
= locals()['_parse_style_%s' % style
.replace('-', '_')]
460 warnings
.warn('Style attribute "%s" not yet implemented' % style
)
462 __style_methods
[style
] = method
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] != '#':
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
)
484 def _process_img(self
, attrs
):
485 '''Process a img tag.
489 # Wait maximum 1s for connection
490 socket
.setdefaulttimeout(1)
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
))
498 alt
= attrs
.get('alt', 'Broken image')
500 # Wait 0.1s between each byte
502 f
.fp
._sock
.fp
._sock
.settimeout(0.5)
505 # Max image size = 2 MB (to try to prevent DoS)
506 deadline
= time
.time() + 3
508 if time
.time() > deadline
:
509 gajim
.log
.debug(str('Timeout loading image %s ' % \
512 alt
= attrs
.get('alt', '')
515 alt
+= _('Timeout loading image')
519 except socket
.timeout
, ex
:
520 gajim
.log
.debug('Timeout loading image %s ' % attrs
['src'] + \
522 alt
= attrs
.get('alt', '')
525 alt
+= _('Timeout loading image')
531 if len(mem
) > 2*1024*1024:
532 alt
= attrs
.get('alt', '')
535 alt
+= _('Image is too big')
539 # Caveat: GdkPixbuf is known not to be safe to load
540 # images from network... this program is now potentially
542 loader
= gtk
.gdk
.PixbufLoader()
544 def height_cb(length
):
546 def width_cb(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', '')):
555 elif attr
== 'height':
558 self
._parse
_length
(w
, False, False, 1, 1000, width_cb
)
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
)
571 if isinstance(dims
[1], float):
572 dims
[1] = int(dims
[1]*h
)
575 loader
.set_size(*dims
)
577 loader
.connect('size-prepared', set_size
, dims
)
580 pixbuf
= loader
.get_pixbuf()
581 alt
= attrs
.get('alt', '')
582 if pixbuf
is not None:
583 tags
= self
._get
_style
_tags
()
585 tmpmark
= self
.textbuf
.create_mark(None, self
.iter, True)
586 self
.textbuf
.insert_pixbuf(self
.iter, pixbuf
)
587 self
.starting
= False
589 start
= self
.textbuf
.get_iter_at_mark(tmpmark
)
591 self
.textbuf
.apply_tag(tag
, start
, self
.iter)
592 self
.textbuf
.delete_mark(tmpmark
)
594 self
._insert
_text
('[IMG: %s]' % alt
)
595 except Exception, ex
:
596 gajim
.log
.error('Error loading image ' + str(ex
))
598 alt
= attrs
.get('alt', 'Broken image')
605 def _begin_span(self
, style
, tag
=None, id_
=None):
607 self
.styles
.append(tag
)
611 tag
= self
.textbuf
.create_tag(id_
)
613 tag
= self
.textbuf
.create_tag() # we create anonymous tag
614 for attr
, val
in style_iter(style
):
618 method
= self
.__style
_methods
[attr
]
620 warnings
.warn('Style attribute "%s" requested '
621 'but not yet implemented' % attr
)
623 method(self
, tag
, val
)
624 self
.styles
.append(tag
)
629 def _jump_line(self
):
630 self
.textbuf
.insert_with_tags_by_name(self
.iter, '\n', 'eol')
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
()
638 self
.textbuf
.insert_with_tags(self
.iter, text
, *tags
)
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
))
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_
)
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
):
667 if allwhitespace_rx
.match(content
) is not None and self
._starts
_line
():
671 self
.starting
= False
674 def startElement(self
, name
, attrs
):
676 klass
= [i
for i
in attrs
.get('class', ' ').split(' ') if i
]
678 #Add styles defined for classes
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
687 #id_ = attrs.get('id',None)
690 #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
691 href
= attrs
.get('href', None)
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)
701 tag
= self
.textbuf
.create_tag(id_
)
704 elif name
in LIST_ELEMS
:
705 style
+= ';margin-left: 2em'
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', '')
715 self
._begin
_span
(style
, tag
, id_
)
718 pass # handled in endElement
720 pass # handled in endElement
722 if not self
._starts
_line
():
728 elif name
in ('dl', 'ul'):
729 if not self
._starts
_line
():
731 self
.list_counters
.append(None)
733 if not self
._starts
_line
():
735 self
.list_counters
.append(0)
737 if self
.list_counters
[-1] is None:
738 li_head
= unichr(0x2022)
740 self
.list_counters
[-1] += 1
741 li_head
= '%i.' % self
.list_counters
[-1]
742 self
.text
= ' '*len(self
.list_counters
)*4 + li_head
+ ' '
748 if not self
.starting
:
750 elif name
in ('a', 'img', 'body', 'html'):
755 warnings
.warn('Unhandled element "%s"' % name
)
757 def endElement(self
, name
):
758 endPreserving
= False
763 #FIXME: plenty of unused attributes (width, height,...) :)
766 self
.textbuf
.insert_pixbuf(self
.iter, self
.textview
.focus_out_line_pixbuf
)
767 #self._insert_text(u'\u2550'*40)
770 gajim
.log
.debug(str('Error in hr'+e
))
771 elif name
in LIST_ELEMS
:
772 self
.list_counters
.pop()
777 elif name
== 'body' or name
== 'html':
783 elif name
in ('dd', 'dt', ):
789 warnings
.warn("Unhandled element '%s'" % name
)
792 self
.preserve
= False
796 #if not self._starts_line():
799 class HtmlTextView(gtk
.TextView
):
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
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
838 if getattr(tag_
, 'is_anchor', False):
839 is_over_anchor
= True
841 if not is_over_anchor
:
843 text
= getattr(tag
, 'title', False)
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
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
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()
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()
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()
913 text
= anchor
.get_data('plaintext')
920 search_iter
.forward_char()
925 if __name__
== '__main__':
928 from conversation_textview
import ConversationTextview
933 def debug(self
, text
):
935 def warn(self
, text
):
937 def error(self
, text
):
944 htmlview
= ConversationTextview(None)
946 path
= gtkgui_helpers
.get_icon_path('gajim-muc_separator')
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
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
,
960 tags
= htmlview
.tv
.get_iter_at_location(x
, y
).get_tags()
962 htmlview
.tv
.get_window(gtk
.TEXT_WINDOW_TEXT
).set_cursor(
963 gtk
.gdk
.Cursor(gtk
.gdk
.XTERM
))
965 tag_table
= htmlview
.tv
.get_buffer().get_tag_table()
969 htmlview
.tv
.get_window(gtk
.TEXT_WINDOW_TEXT
).set_cursor(
970 gtk
.gdk
.Cursor(gtk
.gdk
.HAND2
))
972 elif tag
== tag_table
.lookup('focus-out-line'):
977 #if line_tooltip.timeout != 0:
978 # Check if we should hide the line tooltip
980 # line_tooltip.hide_tooltip()
981 #if over_line and not line_tooltip.win:
982 # line_tooltip.timeout = gobject.timeout_add(500,
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
:
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'
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'm <span style='color:green'>green</span>
1007 with <span style='font-weight: bold'>envy</span>!
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
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 "A foolish consistency is the hobgoblin of little minds."
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%'
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)'>
1043 </ul><hr /><pre style="background-color:rgb(120,120,120)">def fac(n):
1046 return faciter(n-1, acc*n)
1047 if n<0: raise ValueError('Must be non-negative')
1048 return faciter(n,1)</pre>
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)'>
1056 <li> Two is nested: <ul style='background-color:rgb(200,200,100)'>
1058 <li style='font-size:50%'> Two </li>
1059 <li style='font-size:200%'> Three </li>
1060 <li style='font-size:9999pt'> Four </li>
1062 <li> Three </li></ol>
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)
1073 frame
.set_shadow_type(gtk
.SHADOW_IN
)
1078 w
.set_default_size(400, 300)
1080 w
.connect('destroy', lambda w
: gtk
.main_quit())