1
# -*- coding: utf-8 -*-
3
### Copyright (C) 2005 Gustavo J. A. M. Carneiro
5
### This library is free software; you can redistribute it and/or
6
### modify it under the terms of the GNU Lesser General Public
7
### License as published by the Free Software Foundation; either
8
### version 2.1 of the License, or (at your option) any later version.
10
### This library is distributed in the hope that it will be useful,
11
### but WITHOUT ANY WARRANTY; without even the implied warranty of
12
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
### Lesser General Public License for more details.
15
### You should have received a copy of the GNU Lesser General Public
16
### License along with this library; if not, write to the Free Software
17
### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
20
A gtk.TextView-based renderer for XHTML-IM, as described in:
21
http://www.jabber.org/jeps/jep-0071.html
26
import xml.sax, xml.sax.handler
30
from cStringIO import StringIO
37
__all__ = ['HtmlTextView']
39
whitespace_rx = re.compile("\\s+")
41
if gtk.gdk.screen_height_mm():
42
## pixels = points * display_resolution
43
display_resolution = 0.3514598*(gtk.gdk.screen_height() /
44
float(gtk.gdk.screen_height_mm()))
46
# inaccurate, but better than letting the universe implode
47
display_resolution = 1
50
def _parse_css_color(color):
51
'''_parse_css_color(css_color) -> gtk.gdk.Color'''
52
if color.startswith("rgb(") and color.endswith(')'):
53
r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
54
return gtk.gdk.Color(r, g, b)
55
else: # there's some strange issue with colors starting with double #
56
if color.startswith('##'):
57
color = '#0' + color.lstrip('#')
58
return gtk.gdk.color_parse(color)
63
def __init__(self, textview, startiter, canResize):
64
self.textbuf = textview.get_buffer()
65
self.textview = textview
68
self.styles = [] # a gtk.TextTag or None, for each span level
69
self.list_counters = [] # stack (top at head) of list
70
# counters, or None for unordered list
71
self.canResize = canResize
73
def _parse_style_color(self, tag, value):
74
color = _parse_css_color(value)
75
tag.set_property("foreground-gdk", color)
77
def _parse_style_background_color(self, tag, value):
78
color = _parse_css_color(value)
79
tag.set_property("background-gdk", color)
80
if gtk.gtk_version >= (2, 8):
81
tag.set_property("paragraph-background-gdk", color)
84
if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1):
86
def _get_current_attributes(self):
87
attrs = self.textview.get_default_attributes()
88
self.iter.backward_char()
89
self.iter.get_attributes(attrs)
90
self.iter.forward_char()
95
## Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455
96
def _get_current_style_attr(self, propname, comb_oper=None):
97
tags = [tag for tag in self.styles if tag is not None]
99
is_set_name = propname + "-set"
102
if tag.get_property(is_set_name):
104
value = tag.get_property(propname)
105
if comb_oper is None:
108
value = comb_oper(value, tag.get_property(propname))
111
class _FakeAttrs(object):
112
__slots__ = ("font", "font_scale")
114
def _get_current_attributes(self):
115
attrs = self._FakeAttrs()
116
attrs.font_scale = self._get_current_style_attr("scale",
118
if attrs.font_scale is None:
119
attrs.font_scale = 1.0
120
attrs.font = self._get_current_style_attr("font-desc")
121
if attrs.font is None:
122
attrs.font = self.textview.style.font_desc
126
def __parse_length_frac_size_allocate(self, textview, allocation,
127
frac, callback, args):
128
callback(allocation.width*frac, *args)
130
def _parse_length(self, value, font_relative, callback, *args):
131
'''Parse/calc length, converting to pixels, calls callback(length, *args)
132
when the length is first computed or changes'''
133
if value.endswith('%'):
134
frac = float(value[:-1])/100
136
attrs = self._get_current_attributes()
137
font_size = attrs.font.get_size() / pango.SCALE
138
callback(frac*display_resolution*font_size, *args)
140
## CSS says "Percentage values: refer to width of the closest
141
## block-level ancestor"
142
## This is difficult/impossible to implement, so we use
143
## textview width instead; a reasonable approximation..
144
alloc = self.textview.get_allocation()
145
self.__parse_length_frac_size_allocate(self.textview, alloc,
146
frac, callback, args)
147
self.textview.connect("size-allocate",
148
self.__parse_length_frac_size_allocate,
149
frac, callback, args)
151
elif value.endswith('pt'): # points
152
callback(float(value[:-2])*display_resolution, *args)
154
elif value.endswith('em'): # ems, the height of the element's font
155
attrs = self._get_current_attributes()
156
font_size = attrs.font.get_size() / pango.SCALE
157
callback(float(value[:-2])*display_resolution*font_size, *args)
159
elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
160
## FIXME: figure out how to calculate this correctly
161
## for now 'em' size is used as approximation
162
attrs = self._get_current_attributes()
163
font_size = attrs.font.get_size() / pango.SCALE
164
callback(float(value[:-2])*display_resolution*font_size, *args)
166
elif value.endswith('px'): # pixels
167
callback(int(value[:-2]), *args)
170
warnings.warn("Unable to parse length value '%s'" % value)
172
def __parse_font_size_cb(length, tag):
173
tag.set_property("size-points", length/display_resolution)
174
__parse_font_size_cb = staticmethod(__parse_font_size_cb)
176
def _parse_style_font_size(self, tag, value):
179
"xx-small": pango.SCALE_XX_SMALL,
180
"x-small": pango.SCALE_X_SMALL,
181
"small": pango.SCALE_SMALL,
182
"medium": pango.SCALE_MEDIUM,
183
"large": pango.SCALE_LARGE,
184
"x-large": pango.SCALE_X_LARGE,
185
"xx-large": pango.SCALE_XX_LARGE,
190
attrs = self._get_current_attributes()
191
tag.set_property("scale", scale / attrs.font_scale)
193
if value == 'smaller':
194
tag.set_property("scale", pango.SCALE_SMALL)
196
if value == 'larger':
197
tag.set_property("scale", pango.SCALE_LARGE)
199
self._parse_length(value, True, self.__parse_font_size_cb, tag)
201
def _parse_style_font_style(self, tag, value):
204
"normal": pango.STYLE_NORMAL,
205
"italic": pango.STYLE_ITALIC,
206
"oblique": pango.STYLE_OBLIQUE,
209
warnings.warn("unknown font-style %s" % value)
211
tag.set_property("style", style)
213
def __frac_length_tag_cb(length, tag, propname):
214
tag.set_property(propname, length)
215
__frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
217
def _parse_style_margin_left(self, tag, value):
218
self._parse_length(value, False, self.__frac_length_tag_cb,
221
def _parse_style_margin_right(self, tag, value):
222
self._parse_length(value, False, self.__frac_length_tag_cb,
225
def _parse_style_font_weight(self, tag, value):
226
## TODO: missing 'bolder' and 'lighter'
229
'100': pango.WEIGHT_ULTRALIGHT,
230
'200': pango.WEIGHT_ULTRALIGHT,
231
'300': pango.WEIGHT_LIGHT,
232
'400': pango.WEIGHT_NORMAL,
233
'500': pango.WEIGHT_NORMAL,
234
'600': pango.WEIGHT_BOLD,
235
'700': pango.WEIGHT_BOLD,
236
'800': pango.WEIGHT_ULTRABOLD,
237
'900': pango.WEIGHT_HEAVY,
238
'normal': pango.WEIGHT_NORMAL,
239
'bold': pango.WEIGHT_BOLD,
242
warnings.warn("unknown font-style %s" % value)
244
tag.set_property("weight", weight)
246
def _parse_style_font_family(self, tag, value):
247
tag.set_property("family", value)
249
def _parse_style_text_align(self, tag, value):
252
'left': gtk.JUSTIFY_LEFT,
253
'right': gtk.JUSTIFY_RIGHT,
254
'center': gtk.JUSTIFY_CENTER,
255
'justify': gtk.JUSTIFY_FILL,
258
warnings.warn("Invalid text-align:%s requested" % value)
260
tag.set_property("justification", align)
262
def _parse_style_text_decoration(self, tag, value):
263
tag.set_property("strikethrough", False)
264
tag.set_property("underline", pango.UNDERLINE_NONE)
266
for chunk in value.split(' '):
267
if chunk == "underline":
268
tag.set_property("underline", pango.UNDERLINE_SINGLE)
269
elif chunk == "overline":
270
warnings.warn("text-decoration:overline not implemented")
271
elif chunk == "line-through":
272
tag.set_property("strikethrough", True)
273
elif value == "blink":
274
warnings.warn("text-decoration:blink not implemented")
276
warnings.warn("text-decoration:%s not implemented" % value)
279
## build a dictionary mapping styles to methods, for greater speed
280
__style_methods = dict()
281
for style in ["background-color", "color", "font-family", "font-size",
282
"font-style", "font-weight", "margin-left", "margin-right",
283
"text-align", "text-decoration"]:
285
method = locals()["_parse_style_%s" % style.replace('-', '_')]
287
warnings.warn("Style attribute '%s' not yet implemented" % style)
289
__style_methods[style] = method
293
def _get_style_tags(self):
294
return [tag for tag in self.styles if tag is not None]
297
def _begin_span(self, style, tag=None):
299
self.styles.append(tag)
302
tag = self.textbuf.create_tag()
304
l = [item.split(':', 1) for item in style.split(';')]
305
l = [ x for x in l if len( x ) == 2 ]
307
attr = attr.strip().lower()
308
# strip brake the font-family value
309
# for example Sans Serif -> SansSerif
310
val = val.lstrip().rstrip()
312
method = self.__style_methods[attr]
314
warnings.warn("Style attribute '%s' requested "
315
"but not yet implemented" % attr)
317
method(self, tag, val)
319
self.styles.append(tag)
324
def _insert_text(self, text):
325
tags = self._get_style_tags()
327
self.textbuf.insert_with_tags(self.iter, text, *tags)
329
self.textbuf.insert(self.iter, text)
331
def _flush_text(self):
332
if not self.text: return
333
self._insert_text(self.text.replace('\n', ''))
336
def anchor_menu_copy_link(self, widget, href):
337
clip = gtk.clipboard_get()
340
def anchor_menu_open_link(self, widget, href, type_):
341
self.textview.emit("url-clicked", href, type_)
343
def make_anchor_context_menu(self, event, href, type_):
346
menu_items = gtk.ImageMenuItem( _( "_Open link" ) )
347
menu_items.set_image( gtk.image_new_from_stock( gtk.STOCK_NETWORK, gtk.ICON_SIZE_MENU ))
348
menu.append(menu_items)
349
menu_items.connect("activate", self.anchor_menu_open_link, href, type_)
352
menu_items = gtk.ImageMenuItem( _( "_Copy link location" ))
353
menu_items.set_image( gtk.image_new_from_stock( gtk.STOCK_COPY, gtk.ICON_SIZE_MENU ))
354
menu.append(menu_items)
355
menu_items.connect("activate", self.anchor_menu_copy_link, href)
358
menu.popup(None, None, None, event.button, event.time)
360
def _anchor_event(self, tag, textview, event, iter, href, type_):
361
if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1:
362
self.textview.emit("url-clicked", href, type_)
364
if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
365
self.make_anchor_context_menu( event, href, type_)
369
def _object_event( self, widget, event, attrs ):
370
callbacks = self.textview.customObjectsCallbacks
371
if attrs['type'] in callbacks and callbacks[attrs['type']][1]:
372
callbacks[attrs['type']][1]( widget, event, attrs )
374
def char_data(self, content):
377
def start_element(self, name, attrs):
380
style = attrs['style']
386
tag = self.textbuf.create_tag()
387
tag.set_property('foreground', self.textview.linkColor)
388
tag.set_property('underline', pango.UNDERLINE_SINGLE)
390
type_ = attrs['type']
393
tag.connect('event', self._anchor_event, attrs['href'], type_)
396
self._begin_span(style, tag)
399
pass # handled in endElement
401
if not self.iter.starts_line():
402
self._insert_text("\n")
404
if not self.iter.starts_line():
405
self._insert_text("\n")
409
if not self.iter.starts_line():
410
self._insert_text("\n")
411
self.list_counters.insert(0, None)
413
if not self.iter.starts_line():
414
self._insert_text("\n")
415
self.list_counters.insert(0, 0)
417
if self.list_counters[0] is None:
418
li_head = unichr(0x2022)
420
self.list_counters[0] += 1
421
li_head = "%i." % self.list_counters[0]
422
self.text = ' '*len(self.list_counters)*4 + li_head + ' '
425
## Max image size = 10 MB (to try to prevent DoS)
426
mem = urllib.urlopen(attrs['src']).read(10*1024*1024)
427
## Caveat: GdkPixbuf is known not to be safe to load
428
## images from network... this program is now potentially
430
loader = gtk.gdk.PixbufLoader()
431
loader.write(mem); loader.close()
433
anchor = self.textbuf.create_child_anchor(self.iter)
434
img = AnimatedResizableImage(100, 100, self.canResize)
435
img.set_tooltip(attrs['alt'])
436
img.set_from_custom_animation(loader.get_animation())
438
self.textview.add_child_at_anchor(img, anchor)
439
self.textview.scrollLater()
440
self.textview.emotes[anchor] = img
442
except Exception, ex:
449
elif name == 'object':
450
anchor = self.textbuf.create_child_anchor(self.iter)
451
customObj = self.textview.setCustomObject( attrs['class'], \
452
type=attrs['type'], alternate_text=str(attrs['data']))
454
event_box = gtk.EventBox()
455
event_box.add( customObj )
456
event_box.connect( 'event', self._object_event, attrs.copy() )
457
event_box.set_visible_window( False )
458
event_box.set_above_child( True )
459
event_box.set_events(gtk.gdk.ALL_EVENTS_MASK)
461
self.textview.add_child_at_anchor(event_box, anchor)
469
warnings.warn("Unhandled element '%s'" % name)
471
def end_element(self, name):
474
if not self.iter.starts_line():
475
self._insert_text("\n")
477
if not self.iter.starts_line():
478
self._insert_text("\n")
482
self._insert_text("\n")
484
self.list_counters.pop()
486
self.list_counters.pop()
488
self._insert_text("\n")
491
elif name == 'object':
498
warnings.warn("Unhandled element '%s'" % name)
501
class AnimatedResizableImage(gtk.Image):
502
def __init__(self, maxh=None, maxw=None, canResize=False):
503
gtk.Image.__init__(self)
507
self.canResize = canResize
508
self.edited_dimensions = False
509
self.mustKill = False
510
self.connect('destroy', self._must_destroy)
512
def _must_destroy(self, *args):
515
def set_tooltip(self, tooltiptext):
516
self.set_property('has-tooltip', True)
517
self.set_property('tooltip-text', tooltiptext)
519
def set_from_filename(self, filename):
520
animation = gtk.gdk.PixbufAnimation(filename)
521
if animation.is_static_image():
522
self._place_static(animation)
524
self._start_animation(animation)
526
def set_from_custom_animation(self, animation):
527
if animation.is_static_image():
528
self._place_static(animation)
530
self._start_animation(animation)
532
def _place_static(self, animation):
533
self.h = animation.get_height()
534
self.w = animation.get_width()
535
ratio = float(self.w) / self.h
536
while self.h > self.maxh or self.w > self.maxw:
537
self.edited_dimensions = True
538
if self.h < self.maxh:
542
self.w = self.h * ratio
544
if self.edited_dimensions and self.canResize:
545
self.set_from_pixbuf(animation.get_static_image().scale_simple(\
546
int(self.w), self.h, gtk.gdk.INTERP_BILINEAR))
548
self.set_from_pixbuf(animation.get_static_image())
550
def _start_animation(self, animation):
551
iteran = animation.get_iter()
553
self.h = animation.get_height()
554
self.w = animation.get_width()
555
ratio = float(self.w) / self.h
556
while self.h > self.maxh or self.w > self.maxw:
557
self.edited_dimensions = True
558
if self.h <= self.maxh:
562
self.w = self.h * ratio
563
if not self.edited_dimensions or not self.canResize:
564
self.set_from_animation(animation) # fallback to default handler
567
gobject.timeout_add(iteran.get_delay_time(), self._advance, iteran)
569
def _advance(self, iteran):
571
if not self.mustKill:
572
self.set_from_pixbuf(iteran.get_pixbuf().scale_simple(int(self.w), \
573
self.h, gtk.gdk.INTERP_NEAREST))
575
return False # end the update game.
577
gobject.timeout_add(iteran.get_delay_time(), self._advance, iteran)
581
class CustomEmoticonObject(object):
582
def __init__(self, textview):
584
self.textview = textview
588
def setNewImg(self, path=None, alttext=None, canResize = False):
589
img = AnimatedResizableImage(100, 100, canResize)
590
img.set_tooltip(alttext)
591
img.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_SMALL_TOOLBAR)
593
self.imgs.append(img)
596
self.setImgPath(path)
601
def setImgPath( self, path ):
604
self.pixbuf = gtk.gdk.PixbufAnimation(path)
608
def updateImgs( self ):
609
if self.pixbuf is not None:
610
self.textview.scrollLater()
611
for img in self.imgs:
612
img.set_from_custom_animation(self.pixbuf)
617
class WinkObject(object):
618
def __init__(self, textview):
620
self.textview = textview
624
def setNewImg(self, path=None):
626
img.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_SMALL_TOOLBAR)
628
self.imgs.append(img)
631
self.setImgPath(path)
636
def setImgPath( self, path ):
639
self.pixbuf = gtk.gdk.pixbuf_new_from_file( path )
643
def updateImgs( self ):
644
if self.pixbuf is not None:
645
self.textview.scrollLater()
646
for img in self.imgs:
647
img.set_from_pixbuf(self.pixbuf)
653
class HtmlTextView(gtk.TextView):
654
__gtype_name__ = 'HtmlTextView'
656
'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
659
def __init__(self, controller=None, buff=None, scrolledwin=None ):
660
gtk.TextView.__init__(self, buff)
661
self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
662
self.set_editable(False)
663
self.set_pixels_above_lines(2)
664
self.set_pixels_below_lines(2)
666
self._last_mark = None
667
self._changed_cursor = False
668
self.linkColor = "#0000FF"
670
self.connect_after('copy-clipboard', self._on_copy_clipboard)
673
self.connect("motion-notify-event", self.__motion_notify_event)
674
self.connect("leave-notify-event", self.__leave_event)
675
self.connect("enter-notify-event", self.__motion_notify_event)
676
self.connect("url-clicked", self.on_link_clicked)
678
scrolledwin.connect("expose-event", lambda x, y: self.queue_draw())
680
self.controller = controller
681
self.canResize = controller.config.user['canResizeEmoticons']
683
self.customObjects = {}
684
self.customObjectsCallbacks = {
685
'application/x-emesene-emoticon': (self.customEmoticon,
686
self.customEmoticonEvent),
687
'application/x-emesene-ink': (self.ink, None),
688
'application/x-emesene-wink': (self.wink, None),
691
def customEmoticon(self, textview, id, path=None, alttext=None):
692
if id in self.customObjects:
693
return self.customObjects[id].setNewImg(path, alttext, self.canResize)
695
customEmoticon = CustomEmoticonObject(textview)
696
self.customObjects.update({id: customEmoticon})
697
return customEmoticon.setNewImg(path, alttext, self.canResize)
699
def wink(self, textview, id, path=None, alttext=None):
700
if id in self.customObjects:
701
return self.customObjects[id].setNewImg(path)
703
wink = WinkObject(textview)
704
self.customObjects.update({id: wink})
705
return wink.setNewImg(path)
707
def customEmoticonEvent(self, img, event, attrs):
708
if not attrs['class'] in self.customObjects:
710
path = self.customObjects[attrs['class']].path
711
if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
714
menu_items = gtk.ImageMenuItem(_('_Save as...'))
715
menu_items.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE,
717
menu.append(menu_items)
718
menu_items.connect('activate', self.customEmoticonSave, path,
721
menu.popup(None, None, None, event.button, event.time)
723
def customEmoticonSave( self, widget , path , shortcut ):
724
def _on_ce_edit_cb(response, text=''):
725
'''method called when the edition is done'''
727
if response == stock.ACCEPT:
729
success, msg = self.controller.customEmoticons.create(\
735
dialog.error(_("Empty shortcut"))
737
window = dialog.entry_window(_("New shortcut"), shortcut,
738
_on_ce_edit_cb, _("Shortcut"))
741
def ink( self, textview, id, path=None, alttext=None):
743
img.set_from_pixbuf( gtk.gdk.pixbuf_new_from_file( id ) )
747
def setCustomObject(self, id, path=None, type=None, alternate_text=None):
748
if type in self.customObjectsCallbacks:
749
return self.customObjectsCallbacks[type][0](self, id, path, alternate_text)
751
def getCustomObjects(self):
752
return self.customObjects
754
def on_link_clicked(self, view, url, type_):
757
def __leave_event(self, widget, event):
758
if self._changed_cursor:
759
window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
760
window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
761
self._changed_cursor = False
763
def __motion_notify_event(self, widget, event):
764
x, y, _ = widget.window.get_pointer()
765
x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
766
tags = widget.get_iter_at_location(x, y).get_tags()
768
if getattr(tag, 'is_anchor', False):
769
is_over_anchor = True
772
is_over_anchor = False
773
if not self._changed_cursor and is_over_anchor:
774
window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
775
window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
776
self._changed_cursor = True
777
elif self._changed_cursor and not is_over_anchor:
778
window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
779
window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
780
self._changed_cursor = False
783
def set_background(self, pixbuf):
784
'''Sets a background pixbuf to this htmltextview
785
If you are using this textview inside a ScrolledWindow, remember to
786
pass the "scrolledwin" param to the constructor'''
788
# converts the pixbuf to a server side pixmap
789
pixmap = pixbuf.render_pixmap_and_mask()[0]
791
# gets the text window
792
textwnd = self.get_window(gtk.TEXT_WINDOW_TEXT)
794
# sets background pixmap
795
style = self.get_style()
796
style.bg_pixmap[gtk.STATE_NORMAL] = pixmap
797
style.set_background(textwnd, gtk.STATE_NORMAL)
799
# this works too, but it doesn't apply background under scrollbars
800
#textwnd.set_back_pixmap(pixmap, False)
802
def display_html(self, htmlu):
804
#####################################################################
805
# handle WLM's fucking special chars, otherwise htmltextview breaks #
806
# let me know if you find other fucking unicode shit crap #
807
#####################################################################
809
replace('\x02', '<span style="font-weight: bold;">').\
810
replace('\x1f', '<span style="text-decoration:underline">').\
811
replace('\x0f', '').\
812
replace('\x03', '').\
813
replace('\x04', '').\
816
buffer = self.get_buffer()
817
eob = buffer.get_end_iter()
818
parser = xml.parsers.expat.ParserCreate()
819
handler = HtmlHandler(self, eob, self.canResize)
820
parser.StartElementHandler = handler.start_element
821
parser.EndElementHandler = handler.end_element
822
parser.CharacterDataHandler = handler.char_data
825
if not eob.starts_line():
826
buffer.insert(eob, "\n")
828
def scrollToBottom(self, force=False):
829
if not force and self.isScrollLocked():
831
textbuffer = self.get_buffer()
832
textiter = textbuffer.get_end_iter()
833
mark = textbuffer.create_mark("end", textiter, False)
834
self._last_mark = mark
835
self.scroll_to_mark(mark, 0.05, True, 0.0, 1.0)
836
textbuffer.place_cursor(textiter)
839
def isScrollLocked(self):
840
textiter = self.get_buffer().get_end_iter()
842
rect = self.get_visible_rect()
843
pos = rect.y + rect.height
844
if (pos + 25) < self.get_iter_location(textiter).y:
848
def scrollLater(self):
849
if not self.isScrollLocked():
850
# very ugly but quite effective.
851
gobject.timeout_add(200, self.scrollToBottom, True)
853
def _on_copy_clipboard(self, textview):
854
''' replaces the copied text with a new text with the
855
alt text of the images selected at copying '''
856
buffer = textview.get_buffer()
857
if buffer.get_has_selection():
858
iterStart, iterEnd = buffer.get_selection_bounds()
859
if iterStart.get_offset() > iterEnd.get_offset():
861
iterStart = iterEnd.copy()
862
iterEnd = temp.copy() #set the right begining
864
text = buffer.get_slice(iterStart, iterEnd)
866
#TODO: magic string '\xef\xbf\xbc', object replacement special char
867
while iterStart.forward_search('\xef\xbf\xbc', \
868
gtk.TEXT_SEARCH_VISIBLE_ONLY):
870
iterPos, iterEnd = iterStart.forward_search('\xef\xbf\xbc', \
871
gtk.TEXT_SEARCH_VISIBLE_ONLY)
872
anchor = iterPos.get_child_anchor()
873
if anchor and anchor.get_widgets():
874
widget = anchor.get_widgets()[0]
875
if isinstance(widget, gtk.EventBox):
876
alt = anchor.get_widgets()[0].child.get_tooltip_text()
878
alt = anchor.get_widgets()[0].get_tooltip_text()
882
text = text.replace('\xef\xbf\xbc', alt, 1)
886
clipboard = gtk.clipboard_get()
887
clipboard.set_text(text)
891
self.get_buffer().set_text('')
894
if __name__ == '__main__':
895
sw = gtk.ScrolledWindow()
896
htmlview = HtmlTextView(scrolledwin=sw)
897
def url_cb(view, url, type_):
898
print "url-clicked", url, type_
899
htmlview.connect("url-clicked", url_cb)
901
#~ htmlview.display_html('<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n'
902
#~ ' <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
903
#~ ' <span style="font-size: 500%; font-family: serif">World</span>\n'
905
#~ htmlview.display_html("<br/>")
906
#~ htmlview.display_html("""
907
#~ <p style='font-size:large'>
908
#~ <span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
909
#~ I'm <span style='color:green'>green</span>
910
#~ with <span style='font-weight: bold'>envy</span>!
913
#~ htmlview.display_html("<br/>")
914
#~ htmlview.display_html("""
915
#~ <body xmlns='http://www.w3.org/1999/xhtml'>
916
#~ <p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
917
#~ <p style='margin-left: 5px; margin-right: 2%'>
918
#~ "A foolish consistency is the hobgoblin of little minds."
922
#~ htmlview.display_html("<br/>")
923
#~ htmlview.display_html("""
924
#~ <body xmlns='http://www.w3.org/1999/xhtml'>
925
#~ <p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p>
926
#~ <p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg'
927
#~ alt='A License to Jabber'
933
#~ htmlview.display_html("""
934
#~ <body xmlns='http://www.w3.org/1999/xhtml'>
935
#~ <ul style='background-color:rgb(120,140,100)'>
942
#~ htmlview.display_html("""
943
#~ <body xmlns='http://www.w3.org/1999/xhtml'>
951
htmlview.display_html('<body>If you are reading this, you may be ' \
952
'interested in that useless text that came with htmltextview. ' \
953
'Try uncommenting the code above this line.</body>')
957
sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC)
958
sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC)
959
sw.set_property("border-width", 0)
963
frame.set_shadow_type(gtk.SHADOW_IN)
968
w.set_default_size(400, 300)
972
#pixbuf = gtk.gdk.pixbuf_new_from_file("Auto.jpg")
973
#htmlview.set_background(pixbuf)
975
w.connect("destroy", lambda w: gtk.main_quit())