~ubuntu-branches/ubuntu/oneiric/emesene/oneiric-proposed

« back to all changes in this revision

Viewing changes to emesene/gui/gtkui/Renderers.py

  • Committer: Bazaar Package Importer
  • Author(s): Devid Antonio Filoni
  • Date: 2011-03-03 14:49:13 UTC
  • mfrom: (1.1.9 upstream)
  • Revision ID: james.westby@ubuntu.com-20110303144913-0adl9cmw2s35lvzo
Tags: 2.0~git20110303-0ubuntu1
* New upstream git revision (LP: #728469).
* Remove debian/watch, debian/emesene.xpm, debian/install and
  debian/README.source files.
* Remove 21_svn2451_fix_avatar and 20_dont_build_own_libmimic patches.
* debian/control: modify python to python (>= 2.5) in Build-Depends field.
* debian/control: remove python-libmimic from Recommends field.
* debian/control: modify python-gtk2 (>= 2.10) to python-gtk2 (>= 2.12) in
  Depends field.
* debian/control: add python-appindicator and python-xmpp to Recommends
  field.
* debian/control: add python-papyon (>= 0.5.4) and python-webkit to Depends
  field.
* debian/control: update Description field.
* debian/control: add python-setuptools to Build-Depends field.
* debian/control: move python-dbus and python-notify to Depends field.
* Update debian/copyright file.
* Update debian/links file.
* debian/menu: update description field.
* Bump Standards-Version to 3.9.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
'''renderers for the ContactList'''
 
2
# -*- coding: utf-8 -*-
 
3
 
 
4
#    This file is part of emesene.
 
5
#
 
6
#    emesene is free software; you can redistribute it and/or modify
 
7
#    it under the terms of the GNU General Public License as published by
 
8
#    the Free Software Foundation; either version 3 of the License, or
 
9
#    (at your option) any later version.
 
10
#
 
11
#    emesene is distributed in the hope that it will be useful,
 
12
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
#    GNU General Public License for more details.
 
15
#
 
16
#    You should have received a copy of the GNU General Public License
 
17
#    along with emesene; if not, write to the Free Software
 
18
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
19
 
 
20
import sys
 
21
import gtk
 
22
import pango
 
23
import cairo
 
24
import gobject
 
25
 
 
26
from gui.base import Plus
 
27
import extension
 
28
import Parser
 
29
 
 
30
import logging
 
31
log = logging.getLogger('gtkui.Renderers')
 
32
 
 
33
def replace_markup(markup, arg=None):
 
34
    '''replace the tags defined in gui.base.ContactList'''
 
35
    markup = markup.replace("[$nl]", "\n")
 
36
 
 
37
    markup = markup.replace("[$small]", "<small>")
 
38
    markup = markup.replace("[$/small]", "</small>")
 
39
 
 
40
    if markup.count("[$COLOR=") > 0:
 
41
        hexcolor = color = markup.split("[$COLOR=")[1].split("]")[0]
 
42
        if color.count("#") == 0:
 
43
            hexcolor = "#" + color
 
44
 
 
45
        markup = markup.replace("[$COLOR=" + color + "]", \
 
46
                "<span foreground='" + hexcolor + "'>")
 
47
        markup = markup.replace("[$/COLOR]", "</span>")
 
48
 
 
49
    markup = markup.replace("[$b]", "<b>")
 
50
    markup = markup.replace("[$/b]", "</b>")
 
51
 
 
52
    markup = markup.replace("[$i]", "<i>")
 
53
    markup = markup.replace("[$/i]", "</i>")
 
54
 
 
55
    return markup
 
56
 
 
57
class CellRendererFunction(gtk.GenericCellRenderer):
 
58
    '''
 
59
    CellRenderer that behaves like a label, but apply a function to "markup"
 
60
    to show a modified nick. Its a base class, and it is intended to be
 
61
    inherited by extensions.
 
62
    '''
 
63
    __gproperties__ = {
 
64
            'markup': (gobject.TYPE_STRING,
 
65
                "text",
 
66
                "text we'll display (even with plus markup!)",
 
67
                '', #default value
 
68
                gobject.PARAM_READWRITE),
 
69
            'ellipsize': (gobject.TYPE_BOOLEAN,
 
70
                "",
 
71
                "",
 
72
                True, #default value
 
73
                gobject.PARAM_READWRITE)
 
74
            }
 
75
 
 
76
    property_names = __gproperties__.keys()
 
77
 
 
78
    def __init__(self, function):
 
79
        self.__gobject_init__()
 
80
        gtk.GenericCellRenderer.__init__(self)
 
81
        self.__dict__['markup'] = ''
 
82
        self.function = function
 
83
        self._style_handler_id = None
 
84
        self._selected_flgs = (int(gtk.CELL_RENDERER_SELECTED),
 
85
            int(gtk.CELL_RENDERER_SELECTED) + int(gtk.CELL_RENDERER_PRELIT))
 
86
 
 
87
        self._cached_markup = None
 
88
        self._cached_layout = None
 
89
 
 
90
    def __getattr__(self, name):
 
91
        try:
 
92
            return self.get_property(name)
 
93
        except TypeError:
 
94
            raise AttributeError, name
 
95
 
 
96
    def __setattr__(self, name, value):
 
97
        try:
 
98
            self.set_property(name, value)
 
99
        except TypeError:
 
100
            self.__dict__[name] = value
 
101
 
 
102
    def on_get_size(self, widget, cell_area):
 
103
        '''Returns the size of the cellrenderer'''
 
104
        if not self._style_handler_id:
 
105
            self._style_handler_id = widget.connect('style-set',
 
106
                    self._style_set)
 
107
        
 
108
        layout = self.get_layout(widget)
 
109
        if layout:
 
110
            width, height = layout.get_pixel_size()
 
111
        else:
 
112
            width, height = [0,0]
 
113
        return (0, 0, -1, height + (self.ypad * 2))
 
114
 
 
115
    def do_get_property(self, prop):
 
116
        '''return the value of prop if exists, raise TypeError if not found'''
 
117
        if prop.name not in self.property_names:
 
118
            raise TypeError('No property named %s' % (prop.name,))
 
119
 
 
120
        return self.__dict__[prop.name]
 
121
 
 
122
    def do_set_property(self, prop, value):
 
123
        '''set the value of prop if exists, raise TypeError if not found'''
 
124
        if prop.name not in self.property_names:
 
125
            raise TypeError('No property named %s' % (prop.name,))
 
126
 
 
127
        if prop == 'markup': #plus formatting
 
128
            value = Plus.msnplus_to_dict
 
129
 
 
130
        self.__dict__[prop.name] = value
 
131
 
 
132
    def on_render(self, win, widget, bgnd_area, cell_area, expose_area, flags):
 
133
        '''Called by gtk to render the cell.'''
 
134
        x_coord, y_coord, width, height = cell_area
 
135
        x_coord += self.xpad
 
136
        y_coord += self.ypad
 
137
        width -= self.xpad
 
138
        ctx = win.cairo_create()
 
139
        layout = self.get_layout(widget)
 
140
        if layout:
 
141
            layout.set_width(width  * pango.SCALE)
 
142
            layout.set_in_color_override_mode(flags in self._selected_flgs)
 
143
            layout.draw(ctx, (x_coord, y_coord, width, height))
 
144
 
 
145
    def get_layout(self, widget):
 
146
        '''Gets the Pango layout used in the cell in a TreeView widget.'''
 
147
        layout = SmileyLayout(widget.create_pango_context())
 
148
 
 
149
        if self.markup:
 
150
            try:
 
151
                decorated_markup = self.function(self.markup)
 
152
            except Exception, error:
 
153
                log.error("this nick: '%s' made the parser go crazy, striping. Error: %s" % (
 
154
                        self.markup, error))
 
155
 
 
156
                decorated_markup = Plus.msnplus_strip(self.markup)
 
157
 
 
158
            layout.set_text(decorated_markup)
 
159
            return layout
 
160
 
 
161
    def _style_set(self, widget, previous_style):
 
162
        '''callback to the style-set signal of widget'''
 
163
        self._cached_markup = {}
 
164
        self._cached_layout = {}
 
165
        widget.queue_resize()
 
166
 
 
167
################################################################################
 
168
# emesene1 parsers rock the streets, here they're instanciated and used
 
169
# if you could fix the emesene2 one, you would be very welcome.
 
170
################################################################################
 
171
 
 
172
bigparser = Parser.UnifiedParser()
 
173
mohrtutchy_plus_parser = Plus.MsnPlusMarkupMohrtutchy()
 
174
plus_or_noplus = 1 # 1 means plus, 0 means noplus
 
175
 
 
176
def plus_parse(obj, parser, filterdata):
 
177
    global plus_or_noplus
 
178
    # get a plain string with objects
 
179
    format, objects = filterdata.serialize(filterdata.list)
 
180
 
 
181
    if parser and parser.tags != Parser.TAGS_NONE:
 
182
        # we have markup
 
183
        mohrtutchy_plus_parser.isHtml = False
 
184
        if parser.tags == Parser.TAGS_PANGO and plus_or_noplus:
 
185
            # replace msn plus markup with pango
 
186
            format = mohrtutchy_plus_parser.replaceMarkup(format)
 
187
        else:
 
188
            # remove all msn plus markup
 
189
            format = mohrtutchy_plus_parser.removeMarkup(format)
 
190
 
 
191
        # put back the objects
 
192
        filterdata.list = filterdata.deserialize(format, objects)
 
193
    else:
 
194
        format = mohrtutchy_plus_parser.removeMarkup(format)
 
195
        filterdata.list = filterdata.deserialize(format, objects)
 
196
 
 
197
bigparser.connect('filter', plus_parse)
 
198
 
 
199
def msnplus_to_list(txt, do_parse_emotes=True):
 
200
    '''parte text to a DictObj and return a list of strings and
 
201
    gtk.gdk.Pixbufs'''
 
202
 
 
203
    ########################################
 
204
    # Mohrtutchy hax, it works (not sure how)
 
205
    parser = bigparser.getParser(Parser.unescape(txt), Parser.PangoDataType)
 
206
    parsed_stuff = parser.get(smileys=True)
 
207
 
 
208
    list_stuff = []
 
209
    for item in parsed_stuff:
 
210
        if type(item) is Parser.Smiley:
 
211
            list_stuff.append(item.pixbuf)
 
212
        else:
 
213
            list_stuff.append(replace_markup(item))
 
214
    return list_stuff
 
215
 
 
216
    ########################################
 
217
    # boyska's implementation, quite incomplete.
 
218
    dct = Plus.msnplus(txt, do_parse_emotes)
 
219
 
 
220
    if not do_parse_emotes:
 
221
        return dct.to_xml()
 
222
 
 
223
    items = flatten_tree(dct, [], [])
 
224
 
 
225
    temp = []
 
226
    accum = []
 
227
    for item in items:
 
228
        if type(item) in (str, unicode):
 
229
            temp.append(item)
 
230
        else:
 
231
            text = replace_markup("".join(temp))
 
232
            accum.append(text)
 
233
            accum.append(item)
 
234
            temp = []
 
235
 
 
236
    accum.append(replace_markup("".join(temp)))
 
237
 
 
238
    return accum
 
239
 
 
240
def msnplus_to_plain_text(txt):
 
241
    ''' from a nasty string, returns a nice plain text string without
 
242
    bells and whistles, just text '''
 
243
    return bigparser.getParser(txt).get(escaped=False)
 
244
 
 
245
def flatten_tree(dct, accum, parents):
 
246
    '''convert the tree of markup into a list of string that contain pango
 
247
    markup and pixbufs, if an img tag is found all the parent tags should be
 
248
    closed before the pixbuf and reopened after.
 
249
    example:
 
250
        <b>hi! :D lol</b>
 
251
        should be
 
252
        ["<b>hi! </b>", pixbuf, "<b> lol</b>"]
 
253
    '''
 
254
    def open_tag(tag):
 
255
        attrs = " ".join("%s=\"%s\"" % (attr, value) for attr, value in
 
256
                tag.iteritems() if attr not in ['tag', 'childs'] and value)
 
257
 
 
258
        if attrs:
 
259
            return '<%s %s>' % (tag.tag, attrs)
 
260
        else:
 
261
            return '<%s>' % (tag.tag, )
 
262
 
 
263
    if dct.tag:
 
264
        previous = list()
 
265
        i = len(accum)-1
 
266
 
 
267
        while i>=0 and type(accum[i]) != gtk.gdk.Pixbuf:
 
268
            previous.append(accum[i])
 
269
            i -= 1
 
270
 
 
271
        if dct.tag == "img":
 
272
            closed = ""
 
273
            opened = ""
 
274
            tags = list()
 
275
            index = 0
 
276
 
 
277
            for prev in previous:
 
278
                pos = prev.find("[$")
 
279
 
 
280
                while pos != -1:
 
281
                    index = pos+2
 
282
                    found = prev[index]
 
283
 
 
284
                    if found == "b":
 
285
                        tags.append("b")
 
286
                    elif found == "i":
 
287
                        tags.append("i")
 
288
                    elif found == "s":
 
289
                        tags.append("small")
 
290
                    elif found == "/":
 
291
                        if len(tags) > 0:
 
292
                            tags.pop()
 
293
 
 
294
                    prev = prev[index+1:]
 
295
                    pos = prev.find("[$")
 
296
 
 
297
            while len(tags) > 0:
 
298
                tag = tags.pop()
 
299
                closed = closed+"[$/"+tag+"]"
 
300
                opened = "[$"+tag+"]"+opened
 
301
 
 
302
            closed = closed+"".join("</%s>" % (parent.tag, ) for parent in \
 
303
                parents[::-1] if parent)
 
304
 
 
305
            opened = "".join(open_tag(parent) for parent in \
 
306
                parents if parent)+opened
 
307
 
 
308
            accum += [closed, gtk.gdk.pixbuf_new_from_file(dct.src), opened]
 
309
 
 
310
            return accum
 
311
        else:
 
312
            accum += [open_tag(dct)]
 
313
 
 
314
    for child in dct.childs:
 
315
        if type(child) in (str, unicode):
 
316
            accum += [child]
 
317
        elif dct.tag:
 
318
            flatten_tree(child, accum, parents + [dct])
 
319
        else:
 
320
            flatten_tree(child, accum, parents)
 
321
 
 
322
    if dct.tag:
 
323
        accum += ['</%s>' % dct.tag]
 
324
 
 
325
    return accum
 
326
 
 
327
class CellRendererPlus(CellRendererFunction):
 
328
    '''Nick renderer that parse the MSN+ markup, showing colors, gradients and
 
329
    effects'''
 
330
 
 
331
    def __init__(self):
 
332
        global plus_or_noplus
 
333
        plus_or_noplus = 1
 
334
        CellRendererFunction.__init__(self, msnplus_to_list)
 
335
 
 
336
extension.implements(CellRendererPlus, 'nick renderer')
 
337
 
 
338
gobject.type_register(CellRendererPlus)
 
339
 
 
340
class CellRendererNoPlus(CellRendererFunction):
 
341
    '''Nick renderer that "strip" MSN+ markup, not showing any effect/color,
 
342
    but improving the readability'''
 
343
 
 
344
    def __init__(self):
 
345
        global plus_or_noplus
 
346
        plus_or_noplus = 0
 
347
        CellRendererFunction.__init__(self, msnplus_to_list)
 
348
 
 
349
extension.implements(CellRendererNoPlus, 'nick renderer')
 
350
 
 
351
gobject.type_register(CellRendererNoPlus)
 
352
 
 
353
class SmileyLayout(pango.Layout):
 
354
    '''a pango layout to draw smilies'''
 
355
 
 
356
    def __init__(self, context,
 
357
                 parsed_elements_list = None,
 
358
                 color = None,
 
359
                 override_color = None,
 
360
                 scaling=1.0):
 
361
        pango.Layout.__init__(self, context)
 
362
 
 
363
        self._width = -1
 
364
        self._ellipsize = True
 
365
        self._elayout = pango.Layout(context)
 
366
        self._elayout.set_text('___') #…
 
367
        self._elayout.set_ellipsize(pango.ELLIPSIZE_END)
 
368
        self._elayout.set_width(0)
 
369
        self._in_override = False # color override mode, used for selected text
 
370
        self._base_to_center = 0
 
371
        self._text_height = 0
 
372
        self._smilies = {} # key: (index_pos), value(pixbuf)
 
373
        self._base_attrlist = None # no color
 
374
        self._attrlist = None # with color
 
375
        self._override_attrlist = None # with override color
 
376
 
 
377
        if color is None:
 
378
            self._color = gtk.gdk.Color()
 
379
        else:
 
380
            self._color = color
 
381
 
 
382
        if override_color is None:
 
383
            self._override_color = gtk.gdk.Color()
 
384
        else:
 
385
            self._override_color = override_color
 
386
 
 
387
        self._smilies_scaled = {} # key: (index_pos), value(pixbuf)
 
388
        self._scaling = scaling # relative to ascent + desent, -1 for natural
 
389
        self._is_rtl = False
 
390
 
 
391
        self.set_element_list(parsed_elements_list)
 
392
        self._update_layout()
 
393
 
 
394
    def set_element_list(self, parsed_elements_list=None):
 
395
        ''' Sets Layout Text based on parsed elements '''
 
396
        if parsed_elements_list is None:
 
397
            parsed_elements_list = ['']
 
398
 
 
399
        self._update_base(parsed_elements_list)
 
400
 
 
401
    def set_text(self, text):
 
402
        ''' Sets Layout Text '''
 
403
        self.set_element_list(text)
 
404
 
 
405
    def set_markup(self, markup):
 
406
        ''' Same as set_text() '''
 
407
        self.set_element_list(markup)
 
408
 
 
409
    def set_width(self, width):
 
410
        ''' Set width of layout in pixels, -1 for natural width '''
 
411
        self._width = width
 
412
        self._update_layout()
 
413
 
 
414
    def get_width(self):
 
415
        '''return thw width of layout in pixels, -1 for natural width'''
 
416
        return self._width
 
417
 
 
418
    def set_ellipsize(self, value):
 
419
        ''' Turns Ellipsize ON/OFF '''
 
420
        if value == pango.ELLIPSIZE_END:
 
421
            self._ellipsize = True
 
422
        else:
 
423
            self._ellipsize = False
 
424
 
 
425
        self._update_layout()
 
426
 
 
427
    def set_smiley_scaling(self, smiley_scaling):
 
428
        '''
 
429
        Set smiley scalling relative to ascent + desent,
 
430
        -1 for natural size
 
431
        '''
 
432
        self._scaling = smiley_scaling
 
433
        self._update_smilies()
 
434
 
 
435
    def set_in_color_override_mode(self, in_override):
 
436
        if not in_override == self._in_override:
 
437
            self._in_override = in_override
 
438
            self._update_attributes()
 
439
 
 
440
    def set_colors(self, color=None, override_color=None):
 
441
        if color is None:
 
442
            color = gtk.gdk.Color()
 
443
 
 
444
        if override_color is None:
 
445
            override_color = gtk.gdk.Color()
 
446
 
 
447
        self._color = color
 
448
        self._override_color = override_color
 
449
        self._update_attrlists()
 
450
 
 
451
    def _update_base(self, elements_list=None):
 
452
 
 
453
        if elements_list is None:
 
454
            elements_list = ['']
 
455
 
 
456
        self._smilies = {}
 
457
        self._base_attrlist = pango.AttrList()
 
458
        text = ''
 
459
 
 
460
        if type(elements_list) in (str, unicode):
 
461
            elements_list = [elements_list]
 
462
 
 
463
        for element in elements_list:
 
464
            if type(element) in (str, unicode):
 
465
                try:
 
466
                    attrl, ptxt, unused = pango.parse_markup(element, u'\x00')
 
467
                except:
 
468
                    attrl, ptxt = pango.AttrList(), element
 
469
 
 
470
                #append attribute list
 
471
                shift = len(text)
 
472
                itter = attrl.get_iterator()
 
473
 
 
474
                while True:
 
475
                    attrs = itter.get_attrs()
 
476
 
 
477
                    for attr in attrs:
 
478
                        attr.end_index += shift
 
479
                        attr.start_index += shift
 
480
                        self._base_attrlist.insert(attr)
 
481
 
 
482
                    if not itter.next():
 
483
                        break
 
484
 
 
485
                text += ptxt
 
486
            elif type(element) == gtk.gdk.Pixbuf:
 
487
                self._smilies[len(text)] = element
 
488
                text += '_'
 
489
 
 
490
        pango.Layout.set_text(self, text)
 
491
 
 
492
        if hasattr(pango, 'find_base_dir'):
 
493
            for line in text.splitlines():
 
494
                if (pango.find_base_dir(line, -1) == pango.DIRECTION_RTL):
 
495
                    self._is_rtl = True
 
496
                    break
 
497
        else:
 
498
            self._is_rtl = False
 
499
 
 
500
        logical = self.get_line(0).get_pixel_extents()[1]
 
501
        ascent = pango.ASCENT(logical)
 
502
        decent = pango.DESCENT(logical)
 
503
        self._text_height =  ascent + decent
 
504
        self._base_to_center = (self._text_height / 2) - decent
 
505
        self._update_smilies()
 
506
 
 
507
    def _update_smilies(self):
 
508
        self._base_attrlist.filter(lambda attr: attr.type == pango.ATTR_SHAPE)
 
509
        self._smilies_scaled = {}
 
510
 
 
511
        #set max height of a pixbuf
 
512
        if self._scaling >= 0:
 
513
            max_height = self._text_height * self._scaling
 
514
        else:
 
515
            max_height = sys.maxint
 
516
 
 
517
        for index, pixbuf in self._smilies.iteritems():
 
518
 
 
519
            if pixbuf:
 
520
                height, width = pixbuf.get_height(), pixbuf.get_width()
 
521
                npix = pixbuf.copy()
 
522
 
 
523
                if height > max_height:
 
524
                    cairo_scale = float(max_height) / float(height)
 
525
                    height = int(height * cairo_scale)
 
526
                    width = int(width * cairo_scale)
 
527
                    npix = npix.scale_simple(width, height,
 
528
                            gtk.gdk.INTERP_BILINEAR)
 
529
 
 
530
                self._smilies_scaled[index] = npix
 
531
                rect = (0,
 
532
                    -1 * (self._base_to_center + (height /2)) * pango.SCALE,
 
533
                         width * pango.SCALE, height * pango.SCALE)
 
534
 
 
535
                self._base_attrlist.insert(pango.AttrShape((0, 0, 0, 0),
 
536
                    rect, index, index + 1))
 
537
 
 
538
        self._update_attrlists()
 
539
 
 
540
    def _update_attrlists(self):
 
541
        clr = self._color
 
542
        oclr = self._override_color
 
543
        norm_forground = pango.AttrForeground( clr.red,
 
544
                clr.green, clr.blue, 0, len(self.get_text()))
 
545
        override_forground = pango.AttrForeground( oclr.red,
 
546
                oclr.green, oclr.blue, 0, len(self.get_text()))
 
547
        self._attrlist = pango.AttrList()
 
548
        self._attrlist.insert(norm_forground)
 
549
        self._override_attrlist = pango.AttrList()
 
550
        self._override_attrlist.insert(override_forground)
 
551
        itter = self._base_attrlist.get_iterator()
 
552
 
 
553
        while True:
 
554
            attrs = itter.get_attrs()
 
555
 
 
556
            for attr in attrs:
 
557
                self._attrlist.insert(attr.copy())
 
558
 
 
559
                if not (attr.type in (pango.ATTR_FOREGROUND,
 
560
                    pango.ATTR_BACKGROUND)):
 
561
                    self._override_attrlist.insert(attr.copy())
 
562
 
 
563
            if not itter.next():
 
564
                break
 
565
 
 
566
        self._update_attributes()
 
567
 
 
568
    def _update_attributes(self):
 
569
        if self._in_override:
 
570
            self.set_attributes(self._override_attrlist)
 
571
        else:
 
572
            self.set_attributes(self._attrlist)
 
573
 
 
574
    def _update_layout(self):
 
575
        if self._width >= 0 and self._ellipsize == False: # if true, then wrap
 
576
            pango.Layout.set_width(self, self._width)
 
577
        else:
 
578
            pango.Layout.set_width(self, -1)
 
579
 
 
580
    def get_size(self):
 
581
        natural_width, natural_height = pango.Layout.get_size(self)
 
582
 
 
583
        if self._width >= 0 and self._ellipsize : # if ellipsize
 
584
            return self._width, natural_height
 
585
        else:
 
586
            return natural_width, natural_height
 
587
 
 
588
    def get_pixel_size(self):
 
589
        natural_width, natural_height = pango.Layout.get_pixel_size(self)
 
590
 
 
591
        if self._width >= 0 and self._ellipsize : # if ellipsize
 
592
            return pango.PIXELS(self._width), natural_height
 
593
        else:
 
594
            return natural_width, natural_height
 
595
 
 
596
    def draw(self, ctx, area):
 
597
        x, y, width, height = area
 
598
        pxls = pango.PIXELS
 
599
        ctx.rectangle(x, y , width, height)
 
600
        ctx.clip()
 
601
 
 
602
        if self._is_rtl:
 
603
            layout_width = pango.Layout.get_pixel_size(self)[0]
 
604
            ctx.translate(x + width - layout_width, y)
 
605
        else:
 
606
            ctx.translate(x, y)
 
607
 
 
608
            #Clipping and ellipsation
 
609
            if self._width >= 0:
 
610
                inline, byte = 0, 1
 
611
                X, Y, W, H = 0, 1, 2, 3
 
612
                layout_width = self._width
 
613
                lst = self.get_attributes()
 
614
                e_ascent = pango.ASCENT(
 
615
                        self._elayout.get_line(0).get_pixel_extents()[1])
 
616
                coords = [] # of path in px
 
617
 
 
618
                for i in range(self.get_line_count()):
 
619
                    line = self.get_line(i)
 
620
                    edge = line.x_to_index(layout_width)
 
621
 
 
622
                    if edge[inline]:
 
623
                        #create ellipsize layout with the style of the char
 
624
                        attrlist = pango.AttrList()
 
625
                        itter = lst.get_iterator()
 
626
 
 
627
                        while True:
 
628
                            attrs = itter.get_attrs()
 
629
 
 
630
                            for attr in attrs:
 
631
                                if not attr.type == pango.ATTR_SHAPE:
 
632
                                    start, end = itter.range()
 
633
 
 
634
                                    if start <= edge[byte] < end:
 
635
                                        n_attr = attr.copy()
 
636
                                        n_attr.start_index = 0
 
637
                                        n_attr.end_index = 3
 
638
                                        attrlist.insert(n_attr)
 
639
 
 
640
                            if not itter.next():
 
641
                                break
 
642
 
 
643
                        self._elayout.set_attributes(attrlist)
 
644
                        ellipsize_width = self._elayout.get_size()[0]
 
645
                        edge = line.x_to_index(layout_width - ellipsize_width)
 
646
                        char = self.index_to_pos(edge[byte])
 
647
                        char_x, char_y, char_h = (pxls(char[X]), pxls(char[Y]),
 
648
                            pxls(char[H]))
 
649
                        y1, y2 = char_y, char_y + char_h
 
650
 
 
651
                        if edge[inline]:
 
652
                            x1 = char_x
 
653
                        else:
 
654
                            x1 = 0
 
655
 
 
656
                        coords.append((x1, y1))
 
657
                        coords.append((x1, y2))
 
658
                        line_ascent = pango.ASCENT(line.get_pixel_extents()[1])
 
659
                        ctx.move_to(x1, y1 + line_ascent - e_ascent)
 
660
                        ctx.show_layout(self._elayout)
 
661
                    else:
 
662
                        char = self.index_to_pos(edge[byte])
 
663
                        coords.append((pxls(char[X] + char[W]), pxls(char[Y])))
 
664
                        coords.append((pxls(char[X] + char[W]),
 
665
                            pxls(char[Y] + char[H])))
 
666
                if coords:
 
667
                    ctx.move_to(0, 0)
 
668
 
 
669
                    for x, y in coords:
 
670
                        ctx.line_to(x, y)
 
671
 
 
672
                    ctx.line_to(0, coords[-1][1])
 
673
                    ctx.close_path()
 
674
                    ctx.clip()
 
675
 
 
676
        #layout
 
677
        ctx.move_to(0, 0)
 
678
        ctx.show_layout(self)
 
679
        #smilies
 
680
 
 
681
        for index in self._smilies.keys():
 
682
            try:
 
683
                x, y, width, height = self.index_to_pos(index)
 
684
                pixbuf = self._smilies_scaled[index]
 
685
                tx = pxls(x)
 
686
                ty = pxls(y) + (pxls(height)/2) - (pixbuf.get_height()/2)
 
687
                ctx.set_source_pixbuf(pixbuf, tx, ty)
 
688
                ctx.paint()
 
689
            except Exception, error:
 
690
                log.error("Error when painting smilies: %s" % error)
 
691
 
 
692
class SmileyLabel(gtk.Label):
 
693
    '''Label with smiley support. '''
 
694
 
 
695
    __gsignals__ = { 'size_request' : 'override',
 
696
                     'size-allocate' : 'override',
 
697
                     'expose-event' : 'override'}
 
698
 
 
699
    def __init__(self):
 
700
        gtk.Widget.__init__(self)
 
701
        self._text = ['']
 
702
        self._ellipsize = True
 
703
        self._wrap = True
 
704
        self._smiley_layout = None
 
705
        self.set_flags(self.flags() | gtk.NO_WINDOW)
 
706
        self._smiley_layout = SmileyLayout(self.create_pango_context())
 
707
 
 
708
    def set_ellipsize(self, ellipsize):
 
709
        ''' Sets the ellipsize behavior '''
 
710
        self._ellipsize = ellipsize
 
711
        self.queue_resize()
 
712
 
 
713
    def set_wrap(self, wrap):
 
714
        ''' Sets the wrap behavior '''
 
715
        self._wrap = wrap
 
716
        self.queue_resize()
 
717
 
 
718
    def set_markup(self, text=['']):
 
719
        self.set_text(text)
 
720
 
 
721
    def set_text(self, text=['']):
 
722
        ''' Sets widget text '''
 
723
        self._text = text
 
724
        self.setup_smiley_layout()
 
725
        self.queue_resize()
 
726
 
 
727
    def set_smiley_scaling(self, smiley_scaling):
 
728
        self._smiley_layout.set_smiley_scaling(smiley_scaling)
 
729
        self.queue_resize()
 
730
 
 
731
    def setup_smiley_layout(self):
 
732
        self._smiley_layout.set_element_list(self._text)
 
733
 
 
734
    def do_realize(self):
 
735
        gtk.Widget.do_realize(self)
 
736
        self.set_flags(self.flags() | gtk.REALIZED)
 
737
        self.window = self.get_parent().window
 
738
 
 
739
    def do_style_set(self, prev_style):
 
740
        self._smiley_layout.set_colors(self.style.text[gtk.STATE_NORMAL])
 
741
        self.queue_draw()
 
742
 
 
743
    def do_size_request(self, requisition):
 
744
        self._smiley_layout.set_width(-1)
 
745
        width, height = self._smiley_layout.get_pixel_size()
 
746
        requisition.height = height
 
747
 
 
748
        if self._ellipsize or self._wrap:
 
749
            requisition.width = 0
 
750
        else:
 
751
            requisition.width = width
 
752
 
 
753
    def do_size_allocate(self, allocation):
 
754
        if not (self._ellipsize or self._wrap):
 
755
            self._smiley_layout.set_width(-1)
 
756
            width, height = self._smiley_layout.get_pixel_size()
 
757
            self.set_size_request(width, height)
 
758
        else:
 
759
            if self._ellipsize:
 
760
                self._smiley_layout.set_ellipsize(pango.ELLIPSIZE_END)
 
761
            else:
 
762
                self._smiley_layout.set_ellipsize(pango.ELLIPSIZE_NONE)
 
763
 
 
764
            self._smiley_layout.set_width(allocation.width * pango.SCALE)
 
765
            self.set_size_request(-1, self._smiley_layout.get_pixel_size()[1])
 
766
 
 
767
        gtk.Widget.do_size_allocate(self, allocation)
 
768
 
 
769
    def do_expose_event(self, event):
 
770
        area = self.get_allocation()
 
771
        ctx = event.window.cairo_create()
 
772
        self._smiley_layout.draw(ctx, area)
 
773
 
 
774
gobject.type_register(SmileyLabel)
 
775
 
 
776
#from emesene1 by mariano guerra adapted by cando
 
777
#animation support by cando
 
778
#TODO add transformation field in configuration
 
779
#TODO signals in configuration for transformation changes????
 
780
 
 
781
class AvatarRenderer(gtk.GenericCellRenderer):
 
782
    """Renderer for avatar """
 
783
 
 
784
    __gproperties__ = {
 
785
        'image': (gobject.TYPE_OBJECT, 'The contact image', '', gobject.PARAM_READWRITE),
 
786
        'blocked': (bool, 'Contact Blocked', '', False, gobject.PARAM_READWRITE),
 
787
        'dimention': (gobject.TYPE_INT, 'cell dimentions',
 
788
                    'height width of cell', 0, 96, 32, gobject.PARAM_READWRITE),
 
789
        'offline': (bool, 'Contact is offline', '', False, gobject.PARAM_READWRITE),
 
790
        'radius_factor': (gobject.TYPE_FLOAT,'radius of pixbuf',
 
791
                          '0.0 to 0.5 with 0.1 = 10% of dimention',
 
792
                          0.0, 0.5,0.11, gobject.PARAM_READWRITE),
 
793
         }
 
794
 
 
795
    def __init__(self, cellDimention = 32, cellRadius = 0.11):
 
796
        self.__gobject_init__()
 
797
        self._image = None
 
798
        self._dimention = cellDimention
 
799
        self._radius_factor = cellRadius
 
800
        self._offline = False
 
801
 
 
802
        #icon source used to render grayed out offline avatar
 
803
        self._icon_source = gtk.IconSource()
 
804
        self._icon_source.set_state(gtk.STATE_INSENSITIVE)
 
805
 
 
806
        self.set_property('xpad', 1)
 
807
        self.set_property('ypad', 1)
 
808
 
 
809
        #set up information of statusTransformation
 
810
        self._set_transformation('corner|gray')
 
811
        #self.transId = self._config.connect('change::statusTransformation',
 
812
            #self._transformation_callback)
 
813
 
 
814
    def destroy(self):
 
815
        self._config.disconnect(self.transId)
 
816
        gtk.GenericCellRenderer.destroy(self)
 
817
 
 
818
    def _get_padding(self):
 
819
        return (self.get_property('xpad'), self.get_property('ypad'))
 
820
 
 
821
    def _set_transformation(self, setting):
 
822
        transformation = setting.split('|')
 
823
        self._corner = ('corner' in transformation)
 
824
        self._alpha_status = ('alpha' in transformation)
 
825
        self._gray = ('gray' in transformation)
 
826
 
 
827
    def _transformation_callback(self, config, newvalue, oldvalue):
 
828
        self._set_transformation(newvalue)
 
829
 
 
830
    def do_get_property(self, property):
 
831
        if property.name == 'image':
 
832
            return self._image
 
833
        elif property.name == 'dimention':
 
834
            return self._dimention
 
835
        elif property.name == 'radius-factor':
 
836
            return self._radius_factor
 
837
        elif property.name == 'offline':
 
838
            return self._offline
 
839
        else:
 
840
            raise AttributeError, 'unknown property %s' % property.name
 
841
 
 
842
    def do_set_property(self, property, value):
 
843
        if property.name == 'image':
 
844
            self._image = value
 
845
        elif property.name == 'dimention':
 
846
            self._dimention = value
 
847
        elif property.name == 'radius-factor':
 
848
            self._radius_factor = value
 
849
        elif property.name == 'offline':
 
850
            self._offline = value
 
851
        else:
 
852
            raise AttributeError, 'unknown property %s' % property.name
 
853
 
 
854
    def on_get_size(self, widget, cell_area=None):
 
855
        """Requisition size"""
 
856
        xpad, ypad = self._get_padding()
 
857
 
 
858
        if self._dimention >= 32:
 
859
            width = self._dimention
 
860
        elif self._corner:
 
861
            width = self._dimention * 2
 
862
        else:
 
863
            width = self._dimention
 
864
 
 
865
        height = self._dimention + (ypad * 2)
 
866
 
 
867
        return (0, 0,  width, height)
 
868
 
 
869
    def func(self, model, path, iter, (image, tree)):
 
870
      if model.get_value(iter, 0) == image:
 
871
         self.redraw = 1
 
872
         cell_area = tree.get_cell_area(path, tree.get_column(1))
 
873
         tree.queue_draw_area(cell_area.x, cell_area.y, cell_area.width,
 
874
            cell_area.height)
 
875
 
 
876
    def animation_timeout(self, tree, image):
 
877
       if image.get_storage_type() == gtk.IMAGE_ANIMATION:
 
878
          self.redraw = 0
 
879
          image.get_data('iter').advance()
 
880
          model = tree.get_model()
 
881
          model.foreach(self.func, (image, tree))
 
882
 
 
883
          if self.redraw:
 
884
             gobject.timeout_add(image.get_data('iter').get_delay_time(),
 
885
                self.animation_timeout, tree, image)
 
886
          else:
 
887
             image.set_data('iter', None)
 
888
 
 
889
 
 
890
    def on_render(self, window, widget, bg_area, cell_area, expose_area, flags):
 
891
        """Prepare rendering setting for avatar"""
 
892
        xpad, ypad = self._get_padding()
 
893
        x, y, width, height = cell_area
 
894
        ctx = window.cairo_create()
 
895
        ctx.translate(x, y)
 
896
 
 
897
        avatar = None
 
898
        alpha = 1
 
899
        dim = self._dimention
 
900
 
 
901
        if self._image.get_storage_type() == gtk.IMAGE_ANIMATION:
 
902
            if not self._image.get_data('iter'):
 
903
                animation = self._image.get_animation()
 
904
                self._image.set_data('iter', animation.get_iter())
 
905
                gobject.timeout_add(self._image.get_data('iter').get_delay_time(),
 
906
                   self.animation_timeout, widget, self._image)
 
907
 
 
908
            avatar = self._image.get_data('iter').get_pixbuf()
 
909
        elif self._image.get_storage_type() == gtk.IMAGE_PIXBUF:
 
910
            avatar = self._image.get_pixbuf()
 
911
        else:
 
912
           return
 
913
 
 
914
        if self._gray and self._offline and avatar != None:
 
915
            alpha = 1
 
916
            source = self._icon_source
 
917
            source.set_pixbuf(avatar)
 
918
            direction = widget.get_direction()
 
919
            avatar = widget.style.render_icon(source, direction,
 
920
                gtk.STATE_INSENSITIVE, -1, widget, "gtk-image")
 
921
 
 
922
        if avatar:
 
923
            self._draw_avatar(ctx, avatar, width - dim, ypad, dim,
 
924
                gtk.ANCHOR_CENTER, self._radius_factor, alpha)
 
925
 
 
926
    def _draw_avatar(self, ctx, pixbuf, x, y, dimention,
 
927
                         position = gtk.ANCHOR_CENTER,
 
928
                         radius = 0, alpha = 1):
 
929
        """Render avatar"""
 
930
        ctx.save()
 
931
        ctx.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
 
932
        ctx.translate(x, y)
 
933
 
 
934
        pix_width = pixbuf.get_width()
 
935
        pix_height = pixbuf.get_height()
 
936
 
 
937
        if (pix_width > dimention) or (pix_height > dimention):
 
938
            scale_factor = float(dimention) / max (pix_width,pix_height)
 
939
        else:
 
940
            scale_factor = 1
 
941
 
 
942
        scale_width = pix_width* scale_factor
 
943
        scale_height = pix_height* scale_factor
 
944
 
 
945
        #tranlate position
 
946
        if position in (gtk.ANCHOR_NW, gtk.ANCHOR_W, gtk.ANCHOR_SW):
 
947
            x = 0
 
948
        elif position in (gtk.ANCHOR_N, gtk.ANCHOR_CENTER, gtk.ANCHOR_S):
 
949
            x = (dimention/2) - (scale_width/2)
 
950
        else:
 
951
            x = dimention - scale_width
 
952
        if position in (gtk.ANCHOR_NW, gtk.ANCHOR_N, gtk.ANCHOR_NE):
 
953
            y = 0
 
954
        elif position in (gtk.ANCHOR_E, gtk.ANCHOR_CENTER, gtk.ANCHOR_W):
 
955
            y = (dimention/2) - (scale_height/2)
 
956
        else:
 
957
            y = dimention - scale_height
 
958
 
 
959
        ctx.translate(x, y)
 
960
 
 
961
        if radius > 0 :
 
962
            self._rounded_rectangle(ctx, 0, 0, scale_width, scale_height,
 
963
                self._dimention * radius)
 
964
            ctx.clip()
 
965
 
 
966
        ctx.scale(scale_factor,scale_factor)
 
967
        ctx.set_source_pixbuf(pixbuf, 0, 0)
 
968
        ctx.paint_with_alpha(alpha)
 
969
        ctx.restore()
 
970
 
 
971
    def _rounded_rectangle(self, cr, x, y, w, h, radius=5):
 
972
        """Create rounded rectangle path"""
 
973
        # http://cairographics.org/cookbook/roundedrectangles/
 
974
        # modified from mono moonlight aka mono silverlight
 
975
        # test limits (without using multiplications)
 
976
        # http://graphics.stanford.edu/courses/cs248-98-fall/Final/q1.html
 
977
 
 
978
        ARC_TO_BEZIER = 0.55228475
 
979
 
 
980
        if radius > (min(w,h)/2):
 
981
            radius = (min(w,h)/2)
 
982
 
 
983
        #approximate (quite close) the arc using a bezier curve
 
984
        c = ARC_TO_BEZIER * radius
 
985
 
 
986
        cr.new_path();
 
987
        cr.move_to ( x + radius, y)
 
988
        cr.rel_line_to ( w - 2 * radius, 0.0)
 
989
        cr.rel_curve_to ( c, 0.0, radius, c, radius, radius)
 
990
        cr.rel_line_to ( 0, h - 2 * radius)
 
991
        cr.rel_curve_to ( 0.0, c, c - radius, radius, -radius, radius)
 
992
        cr.rel_line_to ( -w + 2 * radius, 0)
 
993
        cr.rel_curve_to ( -c, 0, -radius, -c, -radius, -radius)
 
994
        cr.rel_line_to (0, -h + 2 * radius)
 
995
        cr.rel_curve_to (0.0, -c, radius - c, -radius, radius, -radius)
 
996
        cr.close_path ()
 
997
 
 
998
gobject.type_register(AvatarRenderer)