~juliank/software-center/debian

« back to all changes in this revision

Viewing changes to softwarecenter/ui/gtk3/widgets/cellrenderers.py

  • Committer: Julian Andres Klode
  • Date: 2011-11-20 13:34:41 UTC
  • mfrom: (429.62.1824 software-center)
  • Revision ID: jak@debian.org-20111120133441-npw6j3nmd8v75yav
Merge from 2.0.7 up to 5.1.2 pre-release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2011 Canonical
 
2
#
 
3
# Authors:
 
4
#  Matthew McGowan
 
5
#  Michael Vogt
 
6
#
 
7
# This program is free software; you can redistribute it and/or modify it under
 
8
# the terms of the GNU General Public License as published by the Free Software
 
9
# Foundation; version 3.
 
10
#
 
11
# This program is distributed in the hope that it will be useful, but WITHOUT
 
12
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
13
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
14
# details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License along with
 
17
# this program; if not, write to the Free Software Foundation, Inc.,
 
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
19
 
 
20
from gi.repository import Gtk, Gdk, GObject, Pango
 
21
 
 
22
from softwarecenter.utils import utf8
 
23
from softwarecenter.ui.gtk3.em import EM
 
24
from softwarecenter.ui.gtk3.models.appstore2 import CategoryRowReference
 
25
 
 
26
from stars import StarRenderer, StarSize
 
27
 
 
28
 
 
29
class CellButtonIDs:
 
30
    INFO = 0
 
31
    ACTION = 1
 
32
 
 
33
 
 
34
# custom cell renderer to support dynamic grow
 
35
class CellRendererAppView(Gtk.CellRendererText):
 
36
 
 
37
    # x, y offsets for the overlay icon
 
38
    OVERLAY_XO = OVERLAY_YO = 2
 
39
 
 
40
    # size of the install overlay icon
 
41
    OVERLAY_SIZE = 16
 
42
    
 
43
    # ratings
 
44
    MAX_STARS = 5
 
45
    STAR_SIZE = EM
 
46
 
 
47
    __gproperties__ = {
 
48
        'application' : (GObject.TYPE_PYOBJECT, 'document',
 
49
                         'a xapian document containing pkg information',
 
50
                         GObject.PARAM_READWRITE),
 
51
 
 
52
        'isactive'    : (bool, 'isactive', 'is cell active/selected', False,
 
53
                         GObject.PARAM_READWRITE),
 
54
                     }
 
55
 
 
56
 
 
57
    def __init__(self, icons, layout, show_ratings, overlay_icon_name):
 
58
        GObject.GObject.__init__(self)
 
59
 
 
60
        # geometry-state values
 
61
        self.pixbuf_width = 0
 
62
        self.apptitle_width = 0
 
63
        self.apptitle_height = 0
 
64
        self.normal_height = 0
 
65
        self.selected_height = 0
 
66
        self.show_ratings = show_ratings
 
67
 
 
68
        # button packing
 
69
        self.button_spacing = 0
 
70
        self._buttons = {Gtk.PackType.START: [],
 
71
                         Gtk.PackType.END:   []}
 
72
        self._all_buttons = {}
 
73
 
 
74
        # cache a layout
 
75
        self._layout = layout
 
76
        # star painter, paints stars
 
77
        self._stars = StarRenderer()
 
78
        self._stars.size = StarSize.SMALL
 
79
 
 
80
        # icon/overlay jazz
 
81
        try:
 
82
            self._installed = icons.load_icon(overlay_icon_name,
 
83
                                              self.OVERLAY_SIZE, 0)
 
84
        except GObject.GError:
 
85
            # icon not present in theme, probably because running uninstalled
 
86
            self._installed = icons.load_icon('emblem-system',
 
87
                                              self.OVERLAY_SIZE, 0)
 
88
        return
 
89
 
 
90
    def _layout_get_pixel_width(self, layout):
 
91
        return layout.get_size()[0] / Pango.SCALE
 
92
 
 
93
    def _layout_get_pixel_height(self, layout):
 
94
        return layout.get_size()[1] / Pango.SCALE
 
95
 
 
96
    def _render_category(self,
 
97
            context, cr, app, cell_area, layout, xpad, ypad, is_rtl):
 
98
 
 
99
        layout.set_markup('<b>%s</b>' % app.display_name, -1)
 
100
 
 
101
        # work out max allowable layout width
 
102
        lw = self._layout_get_pixel_width(layout)
 
103
        lh = self._layout_get_pixel_height(layout)
 
104
 
 
105
        if not is_rtl:
 
106
            x = cell_area.x
 
107
        else:
 
108
            x = cell_area.x + cell_area.width - lw
 
109
        y = cell_area.y + (cell_area.height - lh)/2
 
110
 
 
111
        Gtk.render_layout(context, cr, x, y, layout)
 
112
        return
 
113
 
 
114
    def _render_price(self, context, cr, app, layout, cell_area, xpad, ypad, is_rtl):
 
115
        layout.set_markup("US$ %s" % self.model.get_price(app), -1)
 
116
 
 
117
        if is_rtl:
 
118
            x = cell_area.x + xpad
 
119
        else:
 
120
            x = (cell_area.x + cell_area.width - xpad -
 
121
                 self._layout_get_pixel_width(layout))
 
122
 
 
123
        Gtk.render_layout(context, cr,
 
124
                          x, ypad + cell_area.y, layout)
 
125
        return
 
126
 
 
127
    def _render_icon(self, cr, app, cell_area, xpad, ypad, is_rtl):
 
128
        # calc offsets so icon is nicely centered
 
129
        icon = self.model.get_icon(app)
 
130
        xo = (self.pixbuf_width - icon.get_width())/2
 
131
 
 
132
        if not is_rtl:
 
133
            x = cell_area.x + xo + xpad
 
134
        else:
 
135
            x = cell_area.x + cell_area.width + xo - self.pixbuf_width - xpad
 
136
        y = cell_area.y + ypad
 
137
 
 
138
        # draw appicon pixbuf
 
139
        Gdk.cairo_set_source_pixbuf(cr, icon, x, y)
 
140
        cr.paint()
 
141
 
 
142
        # draw overlay if application is installed
 
143
        if self.model.is_installed(app):
 
144
            if not is_rtl:
 
145
                x += (self.pixbuf_width - self.OVERLAY_SIZE + self.OVERLAY_XO)
 
146
            else:
 
147
                x -= self.OVERLAY_XO
 
148
            y += (self.pixbuf_width - self.OVERLAY_SIZE + self.OVERLAY_YO)
 
149
            Gdk.cairo_set_source_pixbuf(cr, self._installed, x, y)
 
150
            cr.paint()
 
151
        return
 
152
 
 
153
    def _render_summary(self, context, cr, app,
 
154
                        cell_area, layout, xpad, ypad,
 
155
                        star_width, is_rtl):
 
156
 
 
157
        layout.set_markup(self.model.get_markup(app), -1)
 
158
 
 
159
        # work out max allowable layout width
 
160
        layout.set_width(-1)
 
161
        lw = self._layout_get_pixel_width(layout)
 
162
        max_layout_width = (cell_area.width - self.pixbuf_width -
 
163
                            3*xpad - star_width)
 
164
                            
 
165
        max_layout_width = cell_area.width - self.pixbuf_width - 3*xpad
 
166
        
 
167
        stats = self.model.get_review_stats(app)
 
168
        if self.show_ratings and stats:
 
169
            max_layout_width -= star_width+6*xpad
 
170
            
 
171
        if self.props.isactive and self.model.get_transaction_progress(app) > 0:
 
172
            action_btn = self.get_button_by_name(CellButtonIDs.ACTION)
 
173
            max_layout_width -= (xpad + action_btn.width) 
 
174
 
 
175
        if lw >= max_layout_width:
 
176
            layout.set_width((max_layout_width)*Pango.SCALE)
 
177
            layout.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
 
178
            lw = max_layout_width
 
179
 
 
180
        apptitle_extents = layout.get_line_readonly(0).get_pixel_extents()[1]
 
181
        self.apptitle_width = apptitle_extents.width
 
182
        self.apptitle_height = apptitle_extents.height
 
183
 
 
184
        if not is_rtl:
 
185
            x = cell_area.x+2*xpad+self.pixbuf_width
 
186
        else:
 
187
            x = cell_area.x+cell_area.width-lw-self.pixbuf_width-2*xpad
 
188
 
 
189
        y = cell_area.y+ypad
 
190
 
 
191
        Gtk.render_layout(context, cr, x, y, layout)
 
192
        return
 
193
 
 
194
    def _render_rating(self, context, cr, app,
 
195
                       cell_area, layout, xpad, ypad,
 
196
                       star_width, star_height, is_rtl):
 
197
 
 
198
        stats = self.model.get_review_stats(app)
 
199
        if not stats: return
 
200
 
 
201
        sr = self._stars
 
202
 
 
203
        if not is_rtl:
 
204
            x = (cell_area.x + 3 * xpad + self.pixbuf_width +
 
205
                 self.apptitle_width)
 
206
        else:
 
207
            x = (cell_area.x + cell_area.width
 
208
                 - 3 * xpad
 
209
                 - self.pixbuf_width
 
210
                 - self.apptitle_width 
 
211
                 - star_width)
 
212
 
 
213
        y = cell_area.y + ypad + (self.apptitle_height-self.STAR_SIZE)/2
 
214
 
 
215
        sr.rating = stats.ratings_average
 
216
        sr.render_star(context, cr, x, y)
 
217
        
 
218
        # and nr-reviews in parenthesis to the right of the title
 
219
        nreviews = stats.ratings_total
 
220
        s = "(%i)" % nreviews
 
221
 
 
222
        layout.set_markup("<small>%s</small>" % s, -1)
 
223
 
 
224
        if not is_rtl:
 
225
            x += xpad+star_width
 
226
        else:
 
227
            x -= xpad+self._layout_get_pixel_width(layout)
 
228
 
 
229
        context.save()
 
230
        context.add_class("cellrenderer-avgrating-label")
 
231
        Gtk.render_layout(context, cr, x, y, layout)
 
232
        context.restore()
 
233
        return
 
234
 
 
235
    def _render_progress(self, context, cr, progress, cell_area, ypad, is_rtl):
 
236
        percent = progress * 0.01
 
237
        # per the spec, the progressbar should be the width of the action button
 
238
        action_btn = self.get_button_by_name(CellButtonIDs.ACTION)
 
239
 
 
240
        x, _, w, h = action_btn.allocation
 
241
        # shift the bar to the top edge
 
242
        y = cell_area.y + ypad
 
243
 
 
244
        context.save()
 
245
        context.add_class("trough")
 
246
 
 
247
        Gtk.render_background(context, cr, x, y, w, h)
 
248
        Gtk.render_frame(context, cr, x, y, w, h)
 
249
 
 
250
        context.restore ()
 
251
 
 
252
        bar_size = w * percent
 
253
 
 
254
        context.save ()
 
255
        context.add_class ("progressbar")
 
256
 
 
257
        if (bar_size > 0):
 
258
            if is_rtl:
 
259
                x += (w - bar_size)
 
260
            Gtk.render_activity(context, cr, x, y, bar_size, h)
 
261
 
 
262
        context.restore ()
 
263
        return
 
264
 
 
265
    def _render_buttons(
 
266
            self, context, cr, cell_area, layout, xpad, ypad, is_rtl):
 
267
 
 
268
        # layout buttons and paint
 
269
        y = cell_area.y+cell_area.height-ypad
 
270
        spacing = self.button_spacing
 
271
 
 
272
        if not is_rtl:
 
273
            start = Gtk.PackType.START
 
274
            end = Gtk.PackType.END
 
275
            xs = cell_area.x + 2 * xpad + self.pixbuf_width
 
276
            xb = cell_area.x + cell_area.width - xpad
 
277
        else:
 
278
            start = Gtk.PackType.END
 
279
            end = Gtk.PackType.START
 
280
            xs = cell_area.x + xpad
 
281
            xb = cell_area.x + cell_area.width - 2 * xpad - self.pixbuf_width
 
282
 
 
283
        for btn in self._buttons[start]:
 
284
            btn.set_position(xs, y-btn.height)
 
285
            btn.render(context, cr, layout)
 
286
            xs += btn.width + spacing
 
287
 
 
288
        for btn in self._buttons[end]:
 
289
            xb -= btn.width
 
290
            btn.set_position(xb, y-btn.height)
 
291
            btn.render(context, cr, layout)
 
292
 
 
293
            xb -= spacing
 
294
        return
 
295
 
 
296
    def set_pixbuf_width(self, w):
 
297
        self.pixbuf_width = w
 
298
        return
 
299
 
 
300
    def set_button_spacing(self, spacing):
 
301
        self.button_spacing = spacing
 
302
        return
 
303
 
 
304
    def get_button_by_name(self, name):
 
305
        if name in self._all_buttons:
 
306
            return self._all_buttons[name]
 
307
        return None
 
308
 
 
309
    def get_buttons(self):
 
310
        btns = ()
 
311
        for k, v in self._buttons.items():
 
312
            btns += tuple(v)
 
313
        return btns
 
314
 
 
315
    def button_pack(self, btn, pack_type=Gtk.PackType.START):
 
316
        self._buttons[pack_type].append(btn)
 
317
        self._all_buttons[btn.name] = btn
 
318
        return
 
319
 
 
320
    def button_pack_start(self, btn):
 
321
        self.button_pack(btn, Gtk.PackType.START)
 
322
        return
 
323
 
 
324
    def button_pack_end(self, btn):
 
325
        self.button_pack(btn, Gtk.PackType.END)
 
326
        return
 
327
 
 
328
    def do_set_property(self, pspec, value):
 
329
        setattr(self, pspec.name, value)
 
330
 
 
331
    def do_get_property(self, pspec):
 
332
        return getattr(self, pspec.name)
 
333
 
 
334
    def do_get_preferred_height_for_width(self, treeview, width):
 
335
 
 
336
        if not self.get_properties("isactive")[0]:
 
337
            return self.normal_height, self.normal_height
 
338
 
 
339
        return self.selected_height, self.selected_height
 
340
 
 
341
    def do_render(self, cr, widget, bg_area, cell_area, flags):
 
342
        app = self.props.application
 
343
        if not app: return
 
344
 
 
345
        self.model = widget.appmodel
 
346
 
 
347
        context = widget.get_style_context()
 
348
        xpad = self.get_property('xpad')
 
349
        ypad = self.get_property('ypad')
 
350
        star_width, star_height = self._stars.get_visible_size(context)
 
351
        is_rtl = widget.get_direction() == Gtk.TextDirection.RTL
 
352
        layout = self._layout
 
353
 
 
354
        # important! ensures correct text rendering, esp. when using hicolor theme
 
355
        #~ if (flags & Gtk.CellRendererState.SELECTED) != 0:
 
356
            #~ # this follows the behaviour that gtk+ uses for states in treeviews
 
357
            #~ if widget.has_focus():
 
358
                #~ state = Gtk.StateFlags.SELECTED
 
359
            #~ else:
 
360
                #~ state = Gtk.StateFlags.ACTIVE
 
361
        #~ else:
 
362
            #~ state = Gtk.StateFlags.NORMAL
 
363
 
 
364
        context.save()
 
365
        #~ context.set_state(state)
 
366
 
 
367
        if isinstance(app, CategoryRowReference):
 
368
            self._render_category(context, cr, app,
 
369
                                  cell_area,
 
370
                                  layout,
 
371
                                  xpad, ypad,
 
372
                                  is_rtl)
 
373
            return
 
374
 
 
375
        self._render_icon(cr, app,
 
376
                          cell_area,
 
377
                          xpad, ypad,
 
378
                          is_rtl)
 
379
 
 
380
        self._render_summary(context, cr, app,
 
381
                             cell_area,
 
382
                             layout,
 
383
                             xpad, ypad,
 
384
                             star_width,
 
385
                             is_rtl)
 
386
                             
 
387
        # only show ratings if we have one
 
388
        if self.show_ratings:
 
389
            self._render_rating(context, cr, app,
 
390
                                cell_area,
 
391
                                layout,
 
392
                                xpad, ypad,
 
393
                                star_width,
 
394
                                star_height,
 
395
                                is_rtl)
 
396
 
 
397
        progress = self.model.get_transaction_progress(app)
 
398
        if progress > 0:
 
399
            self._render_progress(context, cr, progress,
 
400
                                  cell_area,
 
401
                                  ypad,
 
402
                                  is_rtl)
 
403
 
 
404
        elif self.model.is_purchasable(app):
 
405
            self._render_price(context, cr, app, layout,
 
406
                               cell_area, xpad, ypad, is_rtl)
 
407
 
 
408
        # below is the stuff that is only done for the active cell
 
409
        if not self.props.isactive:
 
410
            return
 
411
 
 
412
        self._render_buttons(context, cr,
 
413
                             cell_area,
 
414
                             layout,
 
415
                             xpad, ypad,
 
416
                             is_rtl)
 
417
 
 
418
        context.restore()
 
419
        return
 
420
 
 
421
 
 
422
class CellButtonRenderer(object):
 
423
 
 
424
    def __init__(self, widget, name, use_max_variant_width=True):
 
425
        # use_max_variant_width is currently ignored. assumed to be True
 
426
 
 
427
        self.name = name
 
428
        self.markup_variants = {}
 
429
        self.current_variant = None
 
430
 
 
431
        self.xpad = 12
 
432
        self.ypad = 4
 
433
        self.allocation = [0, 0, 1, 1]
 
434
        self.state = Gtk.StateFlags.NORMAL
 
435
        self.has_focus = False
 
436
        self.visible = True
 
437
 
 
438
        self.widget = widget
 
439
        return
 
440
 
 
441
    def _layout_reset(self, layout):
 
442
        layout.set_width(-1)
 
443
        layout.set_ellipsize(Pango.EllipsizeMode.NONE)
 
444
        return
 
445
 
 
446
    @property
 
447
    def x(self): return self.allocation[0]
 
448
 
 
449
    @property
 
450
    def y(self): return self.allocation[1]
 
451
 
 
452
    @property
 
453
    def width(self): return self.allocation[2]
 
454
 
 
455
    @property
 
456
    def height(self): return self.allocation[3]
 
457
 
 
458
    def configure_geometry(self, layout):
 
459
        self._layout_reset(layout)
 
460
        max_size = (0,0)
 
461
 
 
462
        for k, variant in self.markup_variants.items():
 
463
            safe_markup = GObject.markup_escape_text(utf8(variant))
 
464
            layout.set_markup(safe_markup, -1)
 
465
            size = layout.get_size()
 
466
            max_size = max(max_size, size)
 
467
 
 
468
        w, h = max_size
 
469
        w /= Pango.SCALE
 
470
        h /= Pango.SCALE
 
471
 
 
472
        self.set_size(w+2*self.xpad, h+2*self.ypad)
 
473
        return
 
474
 
 
475
    def point_in(self, px, py):
 
476
        x, y, w, h = self.allocation
 
477
        return (px >= x and px <= x + w and
 
478
                py >= y and py <= y + h)
 
479
 
 
480
    def get_size(self):
 
481
        return self.allocation[2:]
 
482
 
 
483
    def set_position(self, x, y):
 
484
        self.allocation[:2] = int(x), int(y)
 
485
        return
 
486
 
 
487
    def set_size(self, w, h):
 
488
        self.allocation[2:] = int(w), int(h)
 
489
        return
 
490
 
 
491
    def set_state(self, state):
 
492
        if not isinstance(state, Gtk.StateFlags):
 
493
            msg = "state should be of type Gtk.StateFlags, got %s" % type(state)
 
494
            raise TypeError(msg)
 
495
 
 
496
        elif state == self.state: return
 
497
 
 
498
        self.state = state
 
499
        self.widget.queue_draw_area(*self.allocation)
 
500
        return
 
501
 
 
502
    def set_sensitive(self, is_sensitive):
 
503
        if is_sensitive:
 
504
            state = Gtk.StateFlags.PRELIGHT
 
505
        else:
 
506
            state = Gtk.StateFlags.INSENSITIVE
 
507
        self.set_state(state)
 
508
        return
 
509
 
 
510
    def show(self):
 
511
        self.visible = True
 
512
 
 
513
    def hide(self):
 
514
        self.visible = False
 
515
 
 
516
    def set_markup(self, markup):
 
517
        self.markup_variant = (markup,)
 
518
        return
 
519
 
 
520
    def set_markup_variants(self, markup_variants):
 
521
        if not isinstance(markup_variants, dict):
 
522
            msg = type(markup_variants)
 
523
            raise TypeError("Expects a dict object, got %s" % msg)
 
524
 
 
525
        elif not markup_variants: return
 
526
 
 
527
        self.markup_variants = markup_variants
 
528
        self.current_variant = markup_variants.keys()[0]
 
529
        return
 
530
 
 
531
    def set_variant(self, current_var):
 
532
        self.current_variant = current_var
 
533
        return
 
534
 
 
535
    def is_sensitive(self):
 
536
        return self.state == Gtk.StateFlags.INSENSITIVE
 
537
 
 
538
    def render(self, context, cr, layout):
 
539
        if not self.visible:
 
540
            return
 
541
 
 
542
        x, y, width, height = self.allocation
 
543
 
 
544
        context.save()
 
545
        context.add_class("cellrenderer-button")
 
546
 
 
547
        if self.has_focus:
 
548
            context.set_state(self.state | Gtk.StateFlags.FOCUSED)
 
549
        else:
 
550
            context.set_state(self.state)
 
551
 
 
552
        # render background and focal frame if has-focus
 
553
        context.save()
 
554
        context.add_class(Gtk.STYLE_CLASS_BUTTON)
 
555
        Gtk.render_background(context, cr, x, y, width, height)
 
556
        context.restore()
 
557
 
 
558
        if self.has_focus:
 
559
            Gtk.render_focus(context, cr,
 
560
                             x + 3, y + 3,
 
561
                             width - 6, height - 6)
 
562
 
 
563
        # position and render layout markup
 
564
        context.save()
 
565
        context.add_class(Gtk.STYLE_CLASS_BUTTON)
 
566
        layout.set_markup(self.markup_variants[self.current_variant], -1)
 
567
        layout_width = layout.get_pixel_extents()[1].width
 
568
        x = x + (width - layout_width) / 2
 
569
        y += self.ypad
 
570
        Gtk.render_layout(context, cr, x, y, layout)
 
571
        context.restore()
 
572
 
 
573
        context.restore()
 
574
        return