# -*- coding: utf-8 -*-

### Copyright (C) 2005 Gustavo J. A. M. Carneiro
###
### This library is free software; you can redistribute it and/or
### modify it under the terms of the GNU Lesser General Public
### License as published by the Free Software Foundation; either
### version 2.1 of the License, or (at your option) any later version.
###
### This library is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
### Lesser General Public License for more details.
###
### You should have received a copy of the GNU Lesser General Public
### License along with this library; if not, write to the Free Software
### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

'''
A gtk.TextView-based renderer for XHTML-IM, as described in:
  http://www.jabber.org/jeps/jep-0071.html
'''
import gobject
import pango
import gtk
import xml.sax, xml.sax.handler
import re
import warnings
import urllib
from cStringIO import StringIO
import operator
import desktop

import stock
import dialog

__all__ = ['HtmlTextView']

whitespace_rx = re.compile("\\s+")

if gtk.gdk.screen_height_mm():
    ## pixels = points * display_resolution
    display_resolution = 0.3514598*(gtk.gdk.screen_height() /
                        float(gtk.gdk.screen_height_mm()))
else:
    # inaccurate, but better than letting the universe implode
    display_resolution = 1


def _parse_css_color(color):
    '''_parse_css_color(css_color) -> gtk.gdk.Color'''
    if color.startswith("rgb(") and color.endswith(')'):
        r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
        return gtk.gdk.Color(r, g, b)
    else:
        return gtk.gdk.color_parse(color)


class HtmlHandler:

    def __init__(self, textview, startiter, canResize):
        self.textbuf = textview.get_buffer()
        self.textview = textview
        self.iter = startiter
        self.text = ''
        self.styles = [] # a gtk.TextTag or None, for each span level
        self.list_counters = [] # stack (top at head) of list
                                # counters, or None for unordered list
        self.canResize = canResize
        
    def _parse_style_color(self, tag, value):
        color = _parse_css_color(value)
        tag.set_property("foreground-gdk", color)

    def _parse_style_background_color(self, tag, value):
        color = _parse_css_color(value)
        tag.set_property("background-gdk", color)
        if gtk.gtk_version >= (2, 8):
            tag.set_property("paragraph-background-gdk", color)


    if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1):

        def _get_current_attributes(self):
            attrs = self.textview.get_default_attributes()
            self.iter.backward_char()
            self.iter.get_attributes(attrs)
            self.iter.forward_char()
            return attrs

    else:

        ## Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455
        def _get_current_style_attr(self, propname, comb_oper=None):
            tags = [tag for tag in self.styles if tag is not None]
            tags.reverse()
            is_set_name = propname + "-set"
            value = None
            for tag in tags:
                if tag.get_property(is_set_name):
                    if value is None:
                        value = tag.get_property(propname)
                        if comb_oper is None:
                            return value
                    else:
                        value = comb_oper(value, tag.get_property(propname))
            return value

        class _FakeAttrs(object):
            __slots__ = ("font", "font_scale")

        def _get_current_attributes(self):
            attrs = self._FakeAttrs()
            attrs.font_scale = self._get_current_style_attr("scale",
                                                            operator.mul)
            if attrs.font_scale is None:
                attrs.font_scale = 1.0
            attrs.font = self._get_current_style_attr("font-desc")
            if attrs.font is None:
                attrs.font = self.textview.style.font_desc
            return attrs


    def __parse_length_frac_size_allocate(self, textview, allocation,
                                          frac, callback, args):
        callback(allocation.width*frac, *args)

    def _parse_length(self, value, font_relative, callback, *args):
        '''Parse/calc length, converting to pixels, calls callback(length, *args)
        when the length is first computed or changes'''
        if value.endswith('%'):
            frac = float(value[:-1])/100
            if font_relative:
                attrs = self._get_current_attributes()
                font_size = attrs.font.get_size() / pango.SCALE
                callback(frac*display_resolution*font_size, *args)
            else:
                ## CSS says "Percentage values: refer to width of the closest
                ##           block-level ancestor"
                ## This is difficult/impossible to implement, so we use
                ## textview width instead; a reasonable approximation..
                alloc = self.textview.get_allocation()
                self.__parse_length_frac_size_allocate(self.textview, alloc,
                                                       frac, callback, args)
                self.textview.connect("size-allocate",
                                      self.__parse_length_frac_size_allocate,
                                      frac, callback, args)

        elif value.endswith('pt'): # points
            callback(float(value[:-2])*display_resolution, *args)

        elif value.endswith('em'): # ems, the height of the element's font
            attrs = self._get_current_attributes()
            font_size = attrs.font.get_size() / pango.SCALE
            callback(float(value[:-2])*display_resolution*font_size, *args)

        elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
            ## FIXME: figure out how to calculate this correctly
            ##        for now 'em' size is used as approximation
            attrs = self._get_current_attributes()
            font_size = attrs.font.get_size() / pango.SCALE
            callback(float(value[:-2])*display_resolution*font_size, *args)

        elif value.endswith('px'): # pixels
            callback(int(value[:-2]), *args)

        else:
            warnings.warn("Unable to parse length value '%s'" % value)

    def __parse_font_size_cb(length, tag):
        tag.set_property("size-points", length/display_resolution)
    __parse_font_size_cb = staticmethod(__parse_font_size_cb)

    def _parse_style_font_size(self, tag, value):
        try:
            scale = {
                "xx-small": pango.SCALE_XX_SMALL,
                "x-small": pango.SCALE_X_SMALL,
                "small": pango.SCALE_SMALL,
                "medium": pango.SCALE_MEDIUM,
                "large": pango.SCALE_LARGE,
                "x-large": pango.SCALE_X_LARGE,
                "xx-large": pango.SCALE_XX_LARGE,
                } [value]
        except KeyError:
            pass
        else:
            attrs = self._get_current_attributes()
            tag.set_property("scale", scale / attrs.font_scale)
            return
        if value == 'smaller':
            tag.set_property("scale", pango.SCALE_SMALL)
            return
        if value == 'larger':
            tag.set_property("scale", pango.SCALE_LARGE)
            return
        self._parse_length(value, True, self.__parse_font_size_cb, tag)

    def _parse_style_font_style(self, tag, value):
        try:
            style = {
                "normal": pango.STYLE_NORMAL,
                "italic": pango.STYLE_ITALIC,
                "oblique": pango.STYLE_OBLIQUE,
                } [value]
        except KeyError:
            warnings.warn("unknown font-style %s" % value)
        else:
            tag.set_property("style", style)

    def __frac_length_tag_cb(length, tag, propname):
        tag.set_property(propname, length)
    __frac_length_tag_cb = staticmethod(__frac_length_tag_cb)

    def _parse_style_margin_left(self, tag, value):
        self._parse_length(value, False, self.__frac_length_tag_cb,
                           tag, "left-margin")

    def _parse_style_margin_right(self, tag, value):
        self._parse_length(value, False, self.__frac_length_tag_cb,
                           tag, "right-margin")

    def _parse_style_font_weight(self, tag, value):
        ## TODO: missing 'bolder' and 'lighter'
        try:
            weight = {
                '100': pango.WEIGHT_ULTRALIGHT,
                '200': pango.WEIGHT_ULTRALIGHT,
                '300': pango.WEIGHT_LIGHT,
                '400': pango.WEIGHT_NORMAL,
                '500': pango.WEIGHT_NORMAL,
                '600': pango.WEIGHT_BOLD,
                '700': pango.WEIGHT_BOLD,
                '800': pango.WEIGHT_ULTRABOLD,
                '900': pango.WEIGHT_HEAVY,
                'normal': pango.WEIGHT_NORMAL,
                'bold': pango.WEIGHT_BOLD,
                } [value]
        except KeyError:
            warnings.warn("unknown font-style %s" % value)
        else:
            tag.set_property("weight", weight)

    def _parse_style_font_family(self, tag, value):
        tag.set_property("family", value)

    def _parse_style_text_align(self, tag, value):
        try:
            align = {
                'left': gtk.JUSTIFY_LEFT,
                'right': gtk.JUSTIFY_RIGHT,
                'center': gtk.JUSTIFY_CENTER,
                'justify': gtk.JUSTIFY_FILL,
                } [value]
        except KeyError:
            warnings.warn("Invalid text-align:%s requested" % value)
        else:
            tag.set_property("justification", align)

    def _parse_style_text_decoration(self, tag, value):
        tag.set_property("strikethrough", False)
        tag.set_property("underline", pango.UNDERLINE_NONE)

        for chunk in value.split(' '):
            if chunk == "underline":
                tag.set_property("underline", pango.UNDERLINE_SINGLE)
            elif chunk == "overline":
                warnings.warn("text-decoration:overline not implemented")
            elif chunk == "line-through":
                tag.set_property("strikethrough", True)
            elif value == "blink":
                warnings.warn("text-decoration:blink not implemented")
            else:
                warnings.warn("text-decoration:%s not implemented" % value)


    ## build a dictionary mapping styles to methods, for greater speed
    __style_methods = dict()
    for style in ["background-color", "color", "font-family", "font-size",
                  "font-style", "font-weight", "margin-left", "margin-right",
                  "text-align", "text-decoration"]:
        try:
            method = locals()["_parse_style_%s" % style.replace('-', '_')]
        except KeyError:
            warnings.warn("Style attribute '%s' not yet implemented" % style)
        else:
            __style_methods[style] = method
    del style
    ## --

    def _get_style_tags(self):
        return [tag for tag in self.styles if tag is not None]


    def _begin_span(self, style, tag=None):
        if style is None:
            self.styles.append(tag)
            return None
        if tag is None:
            tag = self.textbuf.create_tag()

        l = [item.split(':', 1) for item in style.split(';')]
        l = [ x for x in l if len( x ) == 2 ]
        for attr, val in l:
            attr = attr.strip().lower()
            # strip brake the font-family value
            # for example Sans Serif -> SansSerif
            val = val.lstrip().rstrip()
            try:
                method = self.__style_methods[attr]
            except KeyError:
                warnings.warn("Style attribute '%s' requested "
                              "but not yet implemented" % attr)
            else:
                method(self, tag, val)

        self.styles.append(tag)

    def _end_span(self):
        self.styles.pop(-1)

    def _insert_text(self, text):
        tags = self._get_style_tags()
        if tags:
            self.textbuf.insert_with_tags(self.iter, text, *tags)
        else:
            self.textbuf.insert(self.iter, text)

    def _flush_text(self):
        if not self.text: return
        self._insert_text(self.text.replace('\n', ''))
        self.text = ''

    def anchor_menu_copy_link(self, widget, href):
        clip = gtk.clipboard_get()
        clip.set_text(href)

    def anchor_menu_open_link(self, widget, href, type_):
        self.textview.emit("url-clicked", href, type_)

    def make_anchor_context_menu(self, event, href, type_):
        menu = gtk.Menu()

        menu_items = gtk.ImageMenuItem( _( "_Open link" ) )
        menu_items.set_image( gtk.image_new_from_stock( gtk.STOCK_NETWORK, gtk.ICON_SIZE_MENU ))
        menu.append(menu_items)
        menu_items.connect("activate", self.anchor_menu_open_link, href, type_)
        menu_items.show()

        menu_items = gtk.ImageMenuItem( _( "_Copy link location" ))
        menu_items.set_image( gtk.image_new_from_stock( gtk.STOCK_COPY, gtk.ICON_SIZE_MENU ))
        menu.append(menu_items)
        menu_items.connect("activate", self.anchor_menu_copy_link, href)
        menu_items.show()

        menu.popup(None, None, None, event.button, event.time)

    def _anchor_event(self, tag, textview, event, iter, href, type_):
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1:
            self.textview.emit("url-clicked", href, type_)
            return True
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
            self.make_anchor_context_menu( event, href, type_)
            return True
        return False

    def _object_event( self, widget, event, attrs ):
        callbacks = self.textview.customObjectsCallbacks
        if attrs['type'] in callbacks and callbacks[attrs['type']][1]:
            callbacks[attrs['type']][1]( widget, event, attrs )

    def char_data(self, content):
        self.text += content

    def start_element(self, name, attrs):
        self._flush_text()
        try:
            style = attrs['style']
        except KeyError:
            style = None

        tag = None
        if name == 'a':
            tag = self.textbuf.create_tag()
            tag.set_property('foreground', self.textview.linkColor)
            tag.set_property('underline', pango.UNDERLINE_SINGLE)
            try:
                type_ = attrs['type']
            except KeyError:
                type_ = None
            tag.connect('event', self._anchor_event, attrs['href'], type_)
            tag.is_anchor = True

        self._begin_span(style, tag)

        if name == 'br':
            pass # handled in endElement
        elif name == 'p':
            if not self.iter.starts_line():
                self._insert_text("\n")
        elif name == 'div':
            if not self.iter.starts_line():
                self._insert_text("\n")
        elif name == 'span':
            pass
        elif name == 'ul':
            if not self.iter.starts_line():
                self._insert_text("\n")
            self.list_counters.insert(0, None)
        elif name == 'ol':
            if not self.iter.starts_line():
                self._insert_text("\n")
            self.list_counters.insert(0, 0)
        elif name == 'li':
            if self.list_counters[0] is None:
                li_head = unichr(0x2022)
            else:
                self.list_counters[0] += 1
                li_head = "%i." % self.list_counters[0]
            self.text = ' '*len(self.list_counters)*4 + li_head + ' '
        elif name == 'img':
            try:
                ## Max image size = 10 MB (to try to prevent DoS)
                mem = urllib.urlopen(attrs['src']).read(10*1024*1024)
                ## Caveat: GdkPixbuf is known not to be safe to load
                ## images from network... this program is now potentially
                ## hackable ;)
                loader = gtk.gdk.PixbufLoader()
                loader.write(mem); loader.close()
                
                anchor = self.textbuf.create_child_anchor(self.iter)
                img = AnimatedResizableImage(100, 100, self.canResize)
                img.set_tooltip(attrs['alt'])
                img.set_from_custom_animation(loader.get_animation())
                img.show()
                self.textview.add_child_at_anchor(img, anchor)
                self.textview.scrollLater()
            
            except Exception, ex:
                pixbuf = None
                try:
                    alt = attrs['alt']
                except KeyError:
                    alt = "Broken image"

        elif name == 'object':
            anchor = self.textbuf.create_child_anchor(self.iter)
            customObj = self.textview.setCustomObject( attrs['class'], \
                    type=attrs['type'], alternate_text=str(attrs['data']))
            
            event_box = gtk.EventBox()
            event_box.add( customObj )
            event_box.connect( 'event', self._object_event, attrs.copy() )
            event_box.set_visible_window( False )
            event_box.set_above_child( True )
            event_box.set_events(gtk.gdk.ALL_EVENTS_MASK)
            event_box.show_all()
            self.textview.add_child_at_anchor(event_box, anchor)


        elif name == 'body':
            pass
        elif name == 'a':
            pass
        else:
            warnings.warn("Unhandled element '%s'" % name)

    def end_element(self, name):
        self._flush_text()
        if name == 'p':
            if not self.iter.starts_line():
                self._insert_text("\n")
        elif name == 'div':
            if not self.iter.starts_line():
                self._insert_text("\n")
        elif name == 'span':
            pass
        elif name == 'br':
            self._insert_text("\n")
        elif name == 'ul':
            self.list_counters.pop()
        elif name == 'ol':
            self.list_counters.pop()
        elif name == 'li':
            self._insert_text("\n")
        elif name == 'img':
            pass
        elif name == 'object':
            pass
        elif name == 'body':
            pass
        elif name == 'a':
            pass
        else:
            warnings.warn("Unhandled element '%s'" % name)
        self._end_span()

class AnimatedResizableImage(gtk.Image):
    def __init__(self, maxh=None, maxw=None, canResize=False):
        gtk.Image.__init__(self)    
        
        self.maxh = maxh
        self.maxw = maxw
        self.canResize = canResize
        self.edited_dimensions = False
        self.mustKill = False
        self.connect('destroy', self._must_destroy)
        
    def _must_destroy(self, *args):
        self.mustKill = True
        
    def set_tooltip(self, tooltiptext):
        self.set_property('has-tooltip', True)
        self.set_property('tooltip-text', tooltiptext)

    def set_from_filename(self, filename):
        animation = gtk.gdk.PixbufAnimation(filename)
        if animation.is_static_image():
            self._place_static(animation)
            return
        self._start_animation(animation)
        
    def set_from_custom_animation(self, animation):
        if animation.is_static_image():
            self._place_static(animation)
            return
        self._start_animation(animation)
          
    def _place_static(self, animation):
        self.h = animation.get_height()
        self.w = animation.get_width()
        ratio = float(self.w) / self.h
        while self.h > self.maxh or self.w > self.maxw:
            self.edited_dimensions = True
            if self.h < self.maxh:
                self.h -= 10
            else:
                self.h = self.maxh
            self.w = self.h * ratio
            
        if self.edited_dimensions and self.canResize:
            self.set_from_pixbuf(animation.get_static_image().scale_simple(\
                    int(self.w), self.h, gtk.gdk.INTERP_BILINEAR))    
        else:
            self.set_from_pixbuf(animation.get_static_image())    
            
    def _start_animation(self, animation):
        iteran = animation.get_iter()
        
        self.h = animation.get_height()
        self.w = animation.get_width()
        ratio = float(self.w) / self.h
        while self.h > self.maxh or self.w > self.maxw:
            self.edited_dimensions = True
            if self.h <= self.maxh:
                self.h -= 10
            else:
                self.h = self.maxh
            self.w = self.h * ratio  
        if not self.edited_dimensions or not self.canResize:
            self.set_from_animation(animation) # fallback to default handler
            return
         
        gobject.timeout_add(iteran.get_delay_time(), self._advance, iteran)
    
    def _advance(self, iteran):
        iteran.advance()
        if not self.mustKill:
            self.set_from_pixbuf(iteran.get_pixbuf().scale_simple(int(self.w), \
                    self.h, gtk.gdk.INTERP_NEAREST))
        else:
            return False # end the update game.
        
        gobject.timeout_add(iteran.get_delay_time(), self._advance, iteran)
        return False
  

class CustomEmoticonObject(object):
    def __init__(self, textview):
        self.imgs = []
        self.textview = textview
        self.pixbuf = None
        self.path = None
        
    def setNewImg(self, path=None, alttext=None, canResize = False):
        img = AnimatedResizableImage(100, 100, canResize)
        img.set_tooltip(alttext)
        img.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_SMALL_TOOLBAR)
        img.show()
        self.imgs.append(img)

        if path:
            self.setImgPath(path)

        self.updateImgs()
        return img
        
    def setImgPath( self, path ):
        self.path = path
        try:
            self.pixbuf = gtk.gdk.PixbufAnimation(path)
        except:
            pass

    def updateImgs( self ):
        if self.pixbuf is not None:
            self.textview.scrollLater()
            for img in self.imgs:
                img.set_from_custom_animation(self.pixbuf)

    def getImgs( self ):
        return self.imgs

class WinkObject(object):
    def __init__(self, textview):
        self.imgs = []
        self.textview = textview
        self.pixbuf = None
        self.path = None

    def setNewImg(self, path=None):
        img = gtk.Image()
        img.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_SMALL_TOOLBAR)
        img.show()
        self.imgs.append(img)

        if path:
            self.setImgPath(path)

        self.updateImgs()
        return img

    def setImgPath( self, path ):
        self.path = path
        try:
            self.pixbuf = gtk.gdk.pixbuf_new_from_file( path )
        except:
            pass

    def updateImgs( self ):
        if self.pixbuf is not None:
            self.textview.scrollLater()
            for img in self.imgs:
                img.set_from_pixbuf(self.pixbuf)

    def getImgs( self ):
        return self.imgs


class HtmlTextView(gtk.TextView):
    __gtype_name__ = 'HtmlTextView'
    __gsignals__ = {
        'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
    }

    def __init__(self, controller=None, buff=None, scrolledwin=None ):
        gtk.TextView.__init__(self, buff)
        self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
        self.set_editable(False)
        self.set_pixels_above_lines(2)
        self.set_pixels_below_lines(2)

        self._last_mark = None
        self._changed_cursor = False
        self.linkColor = "#0000FF"

        self.connect("motion-notify-event", self.__motion_notify_event)
        self.connect("leave-notify-event", self.__leave_event)
        self.connect("enter-notify-event", self.__motion_notify_event)
        self.connect("url-clicked", self.on_link_clicked)
        if scrolledwin:
            scrolledwin.connect("expose-event", lambda x, y: self.queue_draw())

        self.controller = controller
        self.canResize = controller.config.user['canResizeEmoticons']
        
        self.customObjects = {}
        self.customObjectsCallbacks = {
            'application/x-emesene-emoticon': (self.customEmoticon,
                                                     self.customEmoticonEvent),
            'application/x-emesene-ink': (self.ink, None),
            'application/x-emesene-wink': (self.wink, None),
        }
    
    def customEmoticon(self, textview, id, path=None, alttext=None):
        if id in self.customObjects:
            return self.customObjects[id].setNewImg(path, alttext, self.canResize)
        else:
            customEmoticon = CustomEmoticonObject(textview)
            self.customObjects.update({id: customEmoticon})
            return customEmoticon.setNewImg(path, alttext, self.canResize)

    def wink(self, textview, id, path=None, alttext=None):
        if id in self.customObjects:
            return self.customObjects[id].setNewImg(path)
        else:
            wink = WinkObject(textview)
            self.customObjects.update({id: wink})
            return wink.setNewImg(path)

    def customEmoticonEvent(self, img, event, attrs):
        if not attrs['class'] in self.customObjects:
            return
        path = self.customObjects[attrs['class']].path
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
            if path:
                menu = gtk.Menu()
                menu_items = gtk.ImageMenuItem(_('_Save as...'))
                menu_items.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE,
                    gtk.ICON_SIZE_MENU))
                menu.append(menu_items)
                menu_items.connect('activate', self.customEmoticonSave, path,
                    attrs['data'])
                menu_items.show()
                menu.popup(None, None, None, event.button, event.time)

    def customEmoticonSave( self, widget , path , shortcut ):
        def _on_ce_edit_cb(response, text=''):
            '''method called when the edition is done'''

            if response == stock.ACCEPT:
                if text:
                    success, msg = self.controller.customEmoticons.create(\
                        text, path, 1)

                    if not success:
                        dialog.error(msg)
                else:
                    dialog.error(_("Empty shortcut"))

        window = dialog.entry_window(_("New shortcut"), shortcut,
            _on_ce_edit_cb, _("Shortcut"))
        window.show()

    def ink( self, textview, id, path=None, alttext=None):
        img = gtk.Image()
        img.set_from_pixbuf( gtk.gdk.pixbuf_new_from_file( id ) )
        img.show()
        return img

    def setCustomObject(self, id, path=None, type=None, alternate_text=None):
        if type in self.customObjectsCallbacks:
            return self.customObjectsCallbacks[type][0](self, id, path, alternate_text)

    def getCustomObjects(self):
        return self.customObjects

    def on_link_clicked(self, view, url, type_):
        desktop.open(url)

    def __leave_event(self, widget, event):
        if self._changed_cursor:
            window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
            window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
            self._changed_cursor = False

    def __motion_notify_event(self, widget, event):
        x, y, _ = widget.window.get_pointer()
        x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
        tags = widget.get_iter_at_location(x, y).get_tags()
        for tag in tags:
            if getattr(tag, 'is_anchor', False):
                is_over_anchor = True
                break
        else:
            is_over_anchor = False
        if not self._changed_cursor and is_over_anchor:
            window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
            window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
            self._changed_cursor = True
        elif self._changed_cursor and not is_over_anchor:
            window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
            window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
            self._changed_cursor = False
        return False

    def set_background(self, pixbuf):
        '''Sets a background pixbuf to this htmltextview
        If you are using this textview inside a ScrolledWindow, remember to
        pass the "scrolledwin" param to the constructor'''

        # converts the pixbuf to a server side pixmap
        pixmap = pixbuf.render_pixmap_and_mask()[0]

        # gets the text window
        textwnd = self.get_window(gtk.TEXT_WINDOW_TEXT)

        # sets background pixmap
        style = self.get_style()
        style.bg_pixmap[gtk.STATE_NORMAL] = pixmap
        style.set_background(textwnd, gtk.STATE_NORMAL)

        # this works too, but it doesn't apply background under scrollbars
        #textwnd.set_back_pixmap(pixmap, False)

    def display_html(self, htmlu):

        #####################################################################
        # handle WLM's fucking special chars, otherwise htmltextview breaks #
        # let me know if you find other fucking unicode shit crap           #
        #####################################################################
        html = htmlu.\
                replace('\x02', '<span style="font-weight: bold;">').\
                replace('\x1f', '<span style="text-decoration:underline">').\
                replace('\x0f', '').\
                replace('\x03', '').\
                replace('\x04', '').\
                replace('\x08', '')

        buffer = self.get_buffer()
        eob = buffer.get_end_iter()
        parser = xml.parsers.expat.ParserCreate()
        handler = HtmlHandler(self, eob, self.canResize)
        parser.StartElementHandler = handler.start_element
        parser.EndElementHandler = handler.end_element
        parser.CharacterDataHandler = handler.char_data
        parser.Parse(html)
        
        if not eob.starts_line():
            buffer.insert(eob, "\n")

    def scrollToBottom(self, force=False):
        if not force and self.isScrollLocked():
            return False
        textbuffer = self.get_buffer()
        textiter = textbuffer.get_end_iter()
        mark = textbuffer.create_mark("end", textiter, False)
        self._last_mark = mark
        self.scroll_to_mark(mark, 0.05, True, 0.0, 1.0)
        textbuffer.place_cursor(textiter)
        return False

    def isScrollLocked(self):
        textiter = self.get_buffer().get_end_iter()
        if self._last_mark:
            rect = self.get_visible_rect()
            pos = rect.y + rect.height
            if (pos + 25) < self.get_iter_location(textiter).y:
                return True
        return False

    def scrollLater(self):
        if not self.isScrollLocked():
            # very ugly but quite effective.
            gobject.timeout_add(200, self.scrollToBottom, True)
            
if __name__ == '__main__':
    sw = gtk.ScrolledWindow()
    htmlview = HtmlTextView(scrolledwin=sw)
    def url_cb(view, url, type_):
        print "url-clicked", url, type_
    htmlview.connect("url-clicked", url_cb)

    #~ htmlview.display_html('<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n'
                          #~ '  <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
                          #~ '  <span style="font-size: 500%; font-family: serif">World</span>\n'
                          #~ '</div>\n')
    #~ htmlview.display_html("<br/>")
    #~ htmlview.display_html("""
      #~ <p style='font-size:large'>
        #~ <span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
        #~ I&apos;m <span style='color:green'>green</span>
        #~ with <span style='font-weight: bold'>envy</span>!
      #~ </p>
        #~ """)
    #~ htmlview.display_html("<br/>")
    #~ htmlview.display_html("""
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
      #~ <p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
      #~ <p style='margin-left: 5px; margin-right: 2%'>
        #~ &quot;A foolish consistency is the hobgoblin of little minds.&quot;
      #~ </p>
    #~ </body>
        #~ """)
    #~ htmlview.display_html("<br/>")
    #~ htmlview.display_html("""
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
      #~ <p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p>
      #~ <p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg'
              #~ alt='A License to Jabber'
              #~ height='261'
              #~ width='537'/></p>
    #~ </body>
        #~ """)

    #~ htmlview.display_html("""
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
      #~ <ul style='background-color:rgb(120,140,100)'>
       #~ <li> One </li>
       #~ <li> Two </li>
       #~ <li> Three </li>
      #~ </ul>
    #~ </body>
        #~ """)
    #~ htmlview.display_html("""
    #~ <body xmlns='http://www.w3.org/1999/xhtml'>
      #~ <ol>
       #~ <li> One </li>
       #~ <li> Two </li>
       #~ <li> Three </li>
      #~ </ol>
    #~ </body>
        #~ """)
    htmlview.display_html('<body>If you are reading this, you may be ' \
        'interested in that useless text that came with htmltextview. ' \
        'Try uncommenting the code above this line.</body>')

    htmlview.show()

    sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC)
    sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC)
    sw.set_property("border-width", 0)
    sw.add(htmlview)
    sw.show()
    frame = gtk.Frame()
    frame.set_shadow_type(gtk.SHADOW_IN)
    frame.show()
    frame.add( sw )
    w = gtk.Window()
    w.add(frame)
    w.set_default_size(400, 300)
    w.show_all()

    # set background
    #pixbuf = gtk.gdk.pixbuf_new_from_file("Auto.jpg")
    #htmlview.set_background(pixbuf)

    w.connect("destroy", lambda w: gtk.main_quit())
    gtk.main()
