Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

beebox / crossover   deb

Repository URL to install this package:

Version: 18.5.0-1 

/ opt / cxoffice / lib / python / htmltextview.py

### Copyright (C) 2005-2007 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 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., 59 Temple Place - Suite 330,
### Boston, MA 02111-1307, 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
import xml.sax.handler
import re
import warnings
from cStringIO import StringIO
import operator

__all__ = ['HtmlTextView']

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

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 HtmlEntityResolver(xml.sax.handler.EntityResolver):
#     def resolveEntity(publicId, systemId):
#        pass

class HtmlHandler(xml.sax.handler.ContentHandler):

    def __init__(self, textview, startiter):
        xml.sax.handler.ContentHandler.__init__(self)
        self.textbuf = textview.get_buffer()
        self.textview = textview
        self.iter = startiter
        self.text = ''
        self.styles = [] # a list of sequences of gtk.TextTag, for each span level
        self.list_counters = [] # stack (top at head) of list
                                # counters, or None for unordered list

    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 tags in self.styles for tag in tags]
            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()
                callback(frac*font_size, True, *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]), False, *args)

        elif value.endswith('em'): # ems, the height of the element's font
            attrs = self._get_current_attributes()
            font_size = attrs.font.get_size()
            callback(float(value[:-2])*font_size, True, *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()
            callback(float(value[:-2])*font_size, True, *args)

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

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

    @staticmethod
    def __parse_font_size_cb(length, pango_units, tag):
        if pango_units:
            tag.set_property("size", length)
        else: # points
            tag.set_property("size-points", length)

    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)

    @staticmethod
    def __frac_length_tag_cb(length, pango_units, tag, propname):
        if pango_units:
            length = length / pango.SCALE
        # else assume points = pixels, whatever
        tag.set_property(propname, length)

    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):
        if value == "none":
            tag.set_property("underline", pango.UNDERLINE_NONE)
            tag.set_property("strikethrough", False)
        elif value == "underline":
            tag.set_property("underline", pango.UNDERLINE_SINGLE)
            tag.set_property("strikethrough", False)
        elif value == "overline":
            warnings.warn("text-decoration:overline not implemented")
            tag.set_property("underline", pango.UNDERLINE_NONE)
            tag.set_property("strikethrough", False)
        elif value == "line-through":
            tag.set_property("underline", pango.UNDERLINE_NONE)
            tag.set_property("strikethrough", True)
        elif value == "blink":
            warnings.warn("text-decoration:blink not implemented")
        else:
            warnings.warn("text-decoration:%s not implemented" % value)

    def _parse_style_vertical_align(self, tag, value):
        if value == 'sub':
            tag.set_property("rise", -10000 / 2)
            tag.set_property("scale", pango.SCALE_SMALL)
        elif value == 'super':
            tag.set_property("rise", 10000 / 2)
            tag.set_property("scale", pango.SCALE_SMALL)
        else:
            warnings.warn("Unsupported or invalid vertical-align:%s requested" % 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", "vertical-align"]:
        try:
            method = locals()["_parse_style_%s" % style.replace('-', '_')]
        except KeyError:
            warnings.warn("Style attribute '%s' not yet implemented" % style)
        else:
            __style_methods[style] = method

    def _get_style_tags(self):
        return [tag for tags in self.styles for tag in tags]

    def _begin_span(self, style, tags=()):
        if style is None:
            self.styles.append(tags)
            return None
        tags = list(tags)
        for item in style.split(';'):
            item = item.strip()
            if item == "":
                continue
            try:
                attr, val = item.split(':', 1)
            except ValueError:
                raise ValueError("the '%s' style is malformed" % item)
            attr = attr.rstrip().lower()
            val = val.lstrip()
            try:
                method = self.__style_methods[attr]
            except KeyError:
                warnings.warn("Style attribute '%s' requested "
                              "but not yet implemented" % attr)
            else:
                tag = self.textbuf.create_tag()
                tags.append(tag)
                method(self, tag, val)
        self.styles.append(tags)

    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.text = self.text.replace('\n', ' ')
        if self.iter.starts_line():
            self.text = self.text.lstrip(' ')
        self._insert_text(self.text)
        self.text = ''

    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
        return False

    def characters(self, content):
        self.text += whitespace_rx.sub(' ', content)

    _TAG_STYLES = {
        'b': 'font-weight: bold',
        'big': 'font-size: large',
        'cite': 'font-style: italic',
        'code': 'font-family: monospace',
        'dfn': 'font-style: italic',
        'em': 'font-style: italic',
        'i': 'font-style: italic',
        'kbd': 'font-family: monospace',
        'samp': 'font-family: monospace',
        'small': 'font-size: small',
        'strong': 'font-weight: bold',
        'sub': 'vertical-align: sub',
        'sup': 'vertical-align: super',
        'u': 'text-decoration: underline',
        'var': 'font-style: italic',
        }

    _NOP_TAGS = frozenset((
            # The above
            'b', 'big', 'cite', 'code', 'dfn', 'em', 'i', 'kbd',
            'samp', 'small', 'strong', 'sub', 'sup', 'u', 'var',
            # Some extra
            'a', 'body', 'span'))

    def startElement(self, name, attrs):
        self._flush_text()

        if 'style' in attrs:
            style = attrs['style']
        elif name in self._TAG_STYLES:
            style = self._TAG_STYLES[name]
        else:
            style = None

        tags = ()
        if name == 'a':
            anchor_tag = self.textbuf.create_tag()
            try:
                type_ = attrs['type']
            except KeyError:
                type_ = None
            anchor_tag.connect('event', self._anchor_event, attrs['href'], type_)
            anchor_tag.is_anchor = True
            tags = (self.textview.link_tag, anchor_tag)

        self._begin_span(style, tags)

        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 == '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:
                # Loading arbitrary images is disabled for security reasons.
                # Apparently GdkPixbuf isn't safe for loading arbitrary images.
                # Anyway, loading from the network would block, which we
                # probably don't want.
                pixbuf = self.textview.images[attrs['src']]
            except Exception:
                pixbuf = None
                try:
                    alt = attrs['alt']
                except KeyError:
                    alt = "Broken image"
            if pixbuf is not None:
                tags = self._get_style_tags()
                if tags:
                    tmpmark = self.textbuf.create_mark(None, self.iter, True)

                self.textbuf.insert_pixbuf(self.iter, pixbuf)

                if tags:
                    start = self.textbuf.get_iter_at_mark(tmpmark)
                    for tag in tags:
                        self.textbuf.apply_tag(tag, start, self.iter)
                    self.textbuf.delete_mark(tmpmark)
            else:
                self._insert_text("[IMG: %s]" % alt)
        elif name in self._NOP_TAGS:
            pass
        else:
            warnings.warn("Unhandled element '%s'" % name)

    def endElement(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 == '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 in self._NOP_TAGS:
            pass
        else:
            warnings.warn("Unhandled element '%s'" % name)
        self._end_span()


class HtmlTextView(gtk.TextView):
    __gtype_name__ = 'HtmlTextView'
    __gsignals__ = {
        'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
    }
    __gproperties__ = {
    'labellike' :
     (gobject.TYPE_BOOLEAN,
      "labellike",
      "Custom CodeWeavers control for label-like appearance",
      False,
      gobject.PARAM_READABLE | gobject.PARAM_WRITABLE | gobject.PARAM_CONSTRUCT)
    }

    def __init__(self):
        gtk.TextView.__init__(self)
        self.set_wrap_mode(gtk.WRAP_CHAR)
        self.set_editable(False)
        self._changed_cursor = False
        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("style-set", self.__style_set)
        self.connect("parent-set", self.__parent_set)
        self._parent_style_handler = None
        self.set_pixels_above_lines(3)
        self.set_pixels_below_lines(3)
        self.images = {} # a dictionary of strings to gdkpixbuf objects for
                         # images that are acceptable in html
        self.link_tag = self.get_buffer().create_tag()
        self.link_tag.set_property('underline', pango.UNDERLINE_SINGLE)

    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 __style_set(self, widget, _previous_style):
        link_color = widget.get_style().lookup_color('link_color')
        if link_color is None:
            link_color = gtk.gdk.color_parse('#0000ff')
        self.link_tag.set_property('foreground-gdk', link_color)

    def __parent_style_set(self, _widget, _previous_style):
        parent = self.get_parent()
        if parent is not None and parent.get_style():
            self.modify_base(gtk.STATE_NORMAL, parent.get_style().bg[gtk.STATE_NORMAL])

    def __parent_set(self, _widget, old_parent):
        if self._parent_style_handler is not None:
            old_parent.disconnect(self._parent_style_handler)
            self._parent_style_handler = None
        if self.get_parent() is not None:
            self._parent_style_handler = self.get_parent().connect('style_set', self.__parent_style_set)
            self.__parent_style_set(self.get_parent(), None)

    def do_set_property(self, prop, value):
        if prop.name == 'labellike':
            self.labellike = value
            if self.labellike:
                self.set_wrap_mode(gtk.WRAP_WORD)
                self.set_editable(False)
                self.set_cursor_visible(False)
                self.__style_set(self, None)
        else:
            super(HtmlTextView, self).do_set_property(prop, value)

    def display_html(self, html):
        buf = self.get_buffer()
        eob = buf.get_end_iter()
        ## this works too if libxml2 is not available
        #parser = xml.sax.make_parser(['drv_libxml2'])
        parser = xml.sax.make_parser()
        # parser.setFeature(xml.sax.handler.feature_validation, True)
        parser.setContentHandler(HtmlHandler(self, eob))
        #parser.setEntityResolver(HtmlEntityResolver())
        # The parser expects XHTML
        html = html.replace('<br>', '<br/>').replace('</br>', '')
        # The word-break tag is not supported
        html = html.replace('<wbr>', '').replace('</wbr>', '')
        # &nbsp; is an important HTML typographic entity (e.g. in French) but
        # is not supported by the XML parser. So try to manually define it
        # along with some other commonly used entities.
        if not html.startswith('<!DOCTYPE'):
            html = '<!DOCTYPE body [ ' + \
                   '<!ENTITY copy "&#xa9;"> ' + \
                   '<!ENTITY hellip "&#x2026;"> ' + \
                   '<!ENTITY mdash "&#x2014;"> ' + \
                   '<!ENTITY nbsp "&#xa0;"> ' + \
                   '<!ENTITY reg "&#xae;"> ' + \
                   '<!ENTITY trade "&#x2122;"> ' + \
                   ']>' + html
        if isinstance(html, unicode):
            # the xml parser has trouble with unicode sometimes
            html = html.encode('utf8')
        parser.parse(StringIO(html))

        if eob.starts_line():
            eob_1 = buf.get_end_iter()
            eob_1.backward_char()
            buf.delete(eob_1, eob)

if gobject.pygtk_version < (2, 8):
    gobject.type_register(HtmlTextView)

def glade_create(_str1, _str2, _int1, _int2):
    widget = HtmlTextView()
    return widget


if __name__ == '__main__':
    htmlview = HtmlTextView()
    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("""
<p>
Can we add button to return in project root directory in filemanager ?
</p>
        """)

    htmlview.display_html('<body><span style="font-family: monospace">|&#xA0;&#xA0;&#xA0;|</span></body>')

    htmlview.show()
    sw = gtk.ScrolledWindow()
    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()
    w.connect("destroy", lambda w: gtk.main_quit())
    gtk.main()