~mvo/software-center/qml

« back to all changes in this revision

Viewing changes to softwarecenter/ui/gtk/widgets/reviews.py

  • Committer: Michael Vogt
  • Date: 2011-10-05 13:08:09 UTC
  • mfrom: (1887.1.603 software-center)
  • Revision ID: michael.vogt@ubuntu.com-20111005130809-0tin9nr00f0uw65b
mergedĀ fromĀ trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
# -*- coding: utf-8 -*-
3
 
 
4
 
# Copyright (C) 2010 Canonical
5
 
#
6
 
# Authors:
7
 
#  Matthew McGowan
8
 
#  Michael Vogt
9
 
#
10
 
# This program is free software; you can redistribute it and/or modify it under
11
 
# the terms of the GNU General Public License as published by the Free Software
12
 
# Foundation; version 3.
13
 
#
14
 
# This program is distributed in the hope that it will be useful, but WITHOUT
15
 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16
 
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17
 
# details.
18
 
#
19
 
# You should have received a copy of the GNU General Public License along with
20
 
# this program; if not, write to the Free Software Foundation, Inc.,
21
 
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22
 
 
23
 
import pygtk
24
 
pygtk.require ("2.0")
25
 
import datetime
26
 
import gobject
27
 
import cairo
28
 
import gtk
29
 
import mkit
30
 
import pango
31
 
import logging
32
 
 
33
 
import gettext
34
 
from gettext import gettext as _
35
 
 
36
 
from mkit import EM, ShapeStar, get_mkit_theme
37
 
from softwarecenter.drawing import alpha_composite, color_floats
38
 
 
39
 
from softwarecenter.utils import (
40
 
    get_language,
41
 
    get_person_from_config,
42
 
    get_nice_date_string, 
43
 
    upstream_version_compare, 
44
 
    upstream_version, 
45
 
    )
46
 
 
47
 
from softwarecenter.netstatus import network_state_is_connected
48
 
from softwarecenter.enums import PkgStates
49
 
from softwarecenter.backend.reviews import UsefulnessCache
50
 
 
51
 
LOG_ALLOCATION = logging.getLogger("softwarecenter.ui.gtk.allocation")
52
 
 
53
 
 
54
 
class StarPainter(object):
55
 
 
56
 
    FILL_EMPTY      = 0
57
 
    FILL_HALF       = 1
58
 
    FILL_FULL       = 2
59
 
 
60
 
    BORDER_OFF = 0
61
 
    BORDER_ON  = 1
62
 
 
63
 
    STYLE_FLAT        = 0
64
 
    STYLE_BIG         = 1
65
 
    STYLE_INTERACTIVE = 2
66
 
 
67
 
 
68
 
    def __init__(self):
69
 
        self.shape = ShapeStar(5, 0.575)
70
 
        self.theme = get_mkit_theme()
71
 
 
72
 
        self.fill = self.FILL_EMPTY
73
 
        self.border = self.BORDER_OFF
74
 
        self.paint_style = self.STYLE_FLAT
75
 
 
76
 
        self.alpha = 1.0
77
 
 
78
 
        self.bg_color = color_floats('#989898')     # gray
79
 
        self.fg_color = color_floats('#FFa000')     # yellow
80
 
        return
81
 
 
82
 
    def _paint_star(self, cr, widget, state, x, y, w, h, alpha=1.0):
83
 
        cr.save()
84
 
 
85
 
        if self.border == self.BORDER_ON:
86
 
            self._paint_star_border(cr, widget, state, x, y, w, h, alpha)
87
 
 
88
 
        if self.paint_style == self.STYLE_INTERACTIVE:
89
 
            self._paint_star_interactive(cr, widget, state, x, y, w, h, alpha)
90
 
 
91
 
        elif self.paint_style == self.STYLE_BIG:
92
 
            self._paint_star_big(cr, widget, state, x, y, w, h, alpha)
93
 
 
94
 
        elif self.paint_style == self.STYLE_FLAT:
95
 
            self._paint_star_flat(cr, widget, state, x, y, w, h, alpha)
96
 
 
97
 
        else:
98
 
            raise AttributeError, 'paint_style value not recognised!'
99
 
 
100
 
        cr.restore()
101
 
        return
102
 
 
103
 
    def _paint_half_star(self, cr, widget, state, x, y, w, h):
104
 
 
105
 
        cr.save()
106
 
 
107
 
        if widget.get_direction() != gtk.TEXT_DIR_RTL:
108
 
            x0 = x
109
 
            x1 = x + w/2
110
 
 
111
 
        else:
112
 
            x0 = x + w/2
113
 
            x1 = x
114
 
 
115
 
        cr.rectangle(x0, y, w/2, h)
116
 
        cr.clip()
117
 
 
118
 
        self.set_fill(self.FILL_FULL)
119
 
        self._paint_star(cr, widget, state, x, y, w, h)
120
 
 
121
 
        cairo.Context.reset_clip(cr)
122
 
 
123
 
        cr.rectangle(x1, y, w/2, h)
124
 
        cr.clip()
125
 
 
126
 
        self.set_fill(self.FILL_EMPTY)
127
 
        self._paint_star(cr, widget, state, x, y, w, h)
128
 
 
129
 
        cairo.Context.reset_clip(cr)
130
 
 
131
 
        self.set_fill(self.FILL_HALF)
132
 
        cr.restore()
133
 
        return
134
 
 
135
 
    def _paint_star_border(self, cr, widget, state, x, y, w, h, alpha):
136
 
        cr.save()
137
 
        cr.set_line_join(cairo.LINE_CAP_ROUND)
138
 
 
139
 
        self.shape.layout(cr, x, y, w, h)
140
 
        sel_color = color_floats(widget.style.base[gtk.STATE_SELECTED])
141
 
 
142
 
        cr.set_source_rgba(*sel_color+(0.65,))
143
 
        cr.set_line_width(4)
144
 
        cr.stroke()
145
 
        cr.restore()
146
 
        return
147
 
 
148
 
    def _paint_star_flat(self, cr, widget, state, x, y, w, h, alpha):
149
 
 
150
 
        if not isinstance(widget, gtk.TreeView):
151
 
            cr.set_source_color(widget.style.white)
152
 
            self.shape.layout(cr, x, y+2, w, h)
153
 
            cr.fill()
154
 
 
155
 
        if self.fill == self.FILL_EMPTY:
156
 
            cr.set_source_rgb(*color_floats(widget.style.mid[gtk.STATE_NORMAL]))
157
 
 
158
 
        else:
159
 
            cr.set_source_rgba(*self.fg_color)
160
 
 
161
 
        self.shape.layout(cr, x, y, w, h)
162
 
        cr.fill()
163
 
 
164
 
        if isinstance(widget, gtk.TreeView) and state == gtk.STATE_SELECTED:
165
 
            self.shape.layout(cr, x-0.5, y-0.5, w+1, h+1)
166
 
            cr.set_line_width(1)
167
 
            cr.set_source_color(widget.style.white)
168
 
            cr.stroke()
169
 
        return
170
 
 
171
 
    def _paint_star_interactive(self, cr, widget, state, *args):
172
 
        if state == gtk.STATE_ACTIVE:
173
 
            self._paint_star_interactive_in(cr, widget, state, *args)
174
 
        else:
175
 
            self._paint_star_interactive_out(cr, widget, state, *args)
176
 
        return
177
 
 
178
 
    def _paint_star_interactive_out(self, cr, widget, state, x, y, w, h, alpha):
179
 
        shape = self.shape
180
 
 
181
 
        # define the color palate
182
 
        if self.fill == self.FILL_FULL:
183
 
            grad0 = white = color_floats(widget.style.white)
184
 
            grad1 = dark = color_floats('#B54D00') # brownish
185
 
 
186
 
            if state == gtk.STATE_PRELIGHT:
187
 
                fill = alpha_composite(self.fg_color+(0.75,), (1,1,1))
188
 
 
189
 
            else:
190
 
                fill = self.fg_color
191
 
 
192
 
        else:
193
 
            theme = self.theme
194
 
 
195
 
            grad0, grad1 = theme.gradients[state]
196
 
            grad0 = grad0.floats()
197
 
            grad1 = grad1.floats()
198
 
 
199
 
            white = theme.light_line[state].floats()
200
 
            dark = theme.dark_line[state].floats()
201
 
            fill = grad0
202
 
 
203
 
        cr.set_line_join(cairo.LINE_CAP_ROUND)
204
 
 
205
 
        if not self.border:
206
 
            # paint bevel
207
 
            shape.layout(cr, x, y+1, w, h)
208
 
            cr.set_line_width(2)
209
 
            light = self.theme.light_line[state].floats()
210
 
            cr.set_source_rgba(*light + (0.9,))
211
 
            cr.stroke()
212
 
 
213
 
        shape.layout(cr, x+1, y+1, w-2, h-2)
214
 
 
215
 
        cr.set_source_rgb(*dark)
216
 
        cr.set_line_width(2)
217
 
        cr.stroke_preserve()
218
 
        cr.stroke()
219
 
 
220
 
        shape.layout(cr, x+1, y+1, w-2, h-2)
221
 
        cr.set_source_rgb(*fill)
222
 
        cr.fill_preserve()
223
 
 
224
 
        lin = cairo.LinearGradient(x, y, x, y+h)
225
 
        lin.add_color_stop_rgba(0.0, *white + (0.5,))
226
 
        lin.add_color_stop_rgba(1.0, *dark + (0.3,))
227
 
 
228
 
        cr.set_source(lin)
229
 
        cr.fill()
230
 
 
231
 
        cr.set_line_width(1)
232
 
 
233
 
        shape.layout(cr, x+1.5, y+1.5, w-3, h-3)
234
 
        lin = cairo.LinearGradient(x, y, x, y+h)
235
 
        lin.add_color_stop_rgba(0.0, *white + (0.6,))
236
 
        lin.add_color_stop_rgba(1.0, *white + (0.15,))
237
 
 
238
 
        cr.set_source(lin)
239
 
        cr.stroke()
240
 
        return
241
 
 
242
 
    def _paint_star_interactive_in(self, cr, widget, state, x, y, w, h, alpha):
243
 
        shape = self.shape
244
 
 
245
 
        # define the color palate
246
 
        #~ black = color_floats(widget.style.black)
247
 
 
248
 
        if self.fill == self.FILL_FULL:
249
 
            dark = color_floats('#B54D00') # brownish
250
 
            darker = alpha_composite(dark+(0.65,), (0,0,0))
251
 
            grad0 = grad1 = alpha_composite(self.fg_color+(0.9,), dark)
252
 
 
253
 
        else:
254
 
            theme = self.theme
255
 
            dark = darker = theme.dark_line[state].floats()
256
 
            grad0, grad1 = theme.gradients[state]
257
 
            grad0 = grad0.floats()
258
 
            grad1 = grad1.floats()
259
 
 
260
 
        cr.set_line_join(cairo.LINE_CAP_ROUND)
261
 
 
262
 
        shape.layout(cr, x+1, y+1, w-2, h-2)
263
 
        cr.set_source_rgba(*dark+(0.35,))
264
 
 
265
 
        cr.set_line_width(3)
266
 
        cr.stroke_preserve()
267
 
 
268
 
        cr.set_source_rgba(*dark+(0.8,))
269
 
        cr.set_line_width(2)
270
 
        cr.stroke()
271
 
 
272
 
        self.shape.layout(cr, x+1, y+1, w-2, h-2)
273
 
 
274
 
        lin = cairo.LinearGradient(x, y, x, y+h)
275
 
        lin.add_color_stop_rgb(0.0, *grad0)
276
 
        lin.add_color_stop_rgb(1.0, *grad1)
277
 
 
278
 
        cr.set_source(lin)
279
 
        cr.fill()
280
 
 
281
 
        cr.set_line_width(2)
282
 
        self.shape.layout(cr, x+1, y+1, w-2, h-2)
283
 
 
284
 
        lin = cairo.LinearGradient(x, y, x, y+h)
285
 
        lin.add_color_stop_rgba(0.0, *darker+(0.175,))
286
 
        lin.add_color_stop_rgba(1.0, *darker+(0.05,))
287
 
        cr.set_source(lin)
288
 
 
289
 
        #~ cr.set_source_rgba(*darker+(0.1*alpha,))
290
 
        cr.stroke()
291
 
        return
292
 
 
293
 
    def _paint_star_big(self, cr, widget, state, x, y, w, h, alpha):
294
 
        if self.fill == self.FILL_FULL:
295
 
            white = (1,1,1)
296
 
            dark = color_floats('#B54D00') # brownish
297
 
            fill = self.fg_color
298
 
 
299
 
        else:
300
 
            white = color_floats(widget.style.white)
301
 
            dark = self.theme.dark_line[state].floats()
302
 
            grad0, grad1 = self.theme.gradients[state]
303
 
            fill = color_floats(widget.style.mid[state])
304
 
 
305
 
        cr.set_line_join(cairo.LINE_CAP_ROUND)
306
 
        self.shape.layout(cr, x+2, y+2, w-4, h-4)
307
 
 
308
 
        cr.set_source_rgba(*white+(0.5,))
309
 
        cr.set_line_width(4)
310
 
        cr.stroke_preserve()
311
 
 
312
 
        cr.set_source_rgb(*dark)
313
 
        cr.set_line_width(2)
314
 
        cr.stroke_preserve()
315
 
 
316
 
        cr.set_source_rgb(*fill)
317
 
        cr.fill_preserve()
318
 
 
319
 
        lin = cairo.LinearGradient(x, y, x, y+h)
320
 
        lin.add_color_stop_rgba(0.0, *white+(0.6,))
321
 
        lin.add_color_stop_rgba(1.0, *white+(0.0,))
322
 
        cr.set_source(lin)
323
 
 
324
 
        cr.fill()
325
 
        return
326
 
 
327
 
    def set_paint_style(self, paint_style):
328
 
        self.paint_style = paint_style
329
 
        return
330
 
 
331
 
    def set_fill(self, fill):
332
 
        self.fill = fill
333
 
        return
334
 
 
335
 
    def set_border(self, border_type):
336
 
        self.border = border_type
337
 
        return
338
 
 
339
 
    def paint_star(self, cr, widget, state, x, y, w, h):
340
 
 
341
 
        if self.fill == self.FILL_HALF:
342
 
            self._paint_half_star(cr, widget, state, x, y, w, h)
343
 
            return
344
 
 
345
 
        self._paint_star(cr, widget, state, x, y, w, h, self.alpha)
346
 
        return
347
 
 
348
 
    def paint_rating(self, cr, widget, state, x, y, star_size, max_stars, rating):
349
 
 
350
 
        sw = star_size[0]
351
 
        direction = widget.get_direction()
352
 
 
353
 
        index = range(0, max_stars)
354
 
        if direction == gtk.TEXT_DIR_RTL: index.reverse()
355
 
 
356
 
        for i in index:
357
 
 
358
 
            if i < int(rating):
359
 
                self.set_fill(StarPainter.FILL_FULL)
360
 
 
361
 
            elif (i == int(rating) and 
362
 
                  rating - int(rating) > 0):
363
 
                self.set_fill(StarPainter.FILL_HALF)
364
 
 
365
 
            else:
366
 
                self.set_fill(StarPainter.FILL_EMPTY)
367
 
 
368
 
            self.paint_star(cr, widget, state, x, y, *star_size)
369
 
            x += sw
370
 
        return
371
 
 
372
 
 
373
 
class StarWidget(gtk.EventBox, StarPainter):
374
 
 
375
 
    def __init__(self, size, is_interactive):
376
 
        gtk.EventBox.__init__(self)
377
 
        StarPainter.__init__(self)
378
 
 
379
 
        self.set_visible_window(False)
380
 
        self.set_size_request(*size)
381
 
 
382
 
        self.is_interactive = is_interactive
383
 
        if is_interactive:
384
 
            self._init_event_handling()
385
 
 
386
 
        self.connect('expose-event', self._on_expose)
387
 
        return
388
 
 
389
 
    def _init_event_handling(self):
390
 
        self.set_flags(gtk.CAN_FOCUS)
391
 
        self.set_events(gtk.gdk.BUTTON_PRESS_MASK|
392
 
                        gtk.gdk.BUTTON_RELEASE_MASK|
393
 
                        gtk.gdk.KEY_RELEASE_MASK|
394
 
                        gtk.gdk.KEY_PRESS_MASK|
395
 
                        gtk.gdk.ENTER_NOTIFY_MASK|
396
 
                        gtk.gdk.LEAVE_NOTIFY_MASK)
397
 
        return
398
 
 
399
 
    def _on_expose(self, widget, event):
400
 
        cr = widget.window.cairo_create()
401
 
        self.draw(cr, self.allocation)
402
 
        del cr
403
 
        return
404
 
 
405
 
    def draw(self, cr, a):
406
 
        w, h = self.get_size_request()
407
 
        x = a.x + (a.width-w)/2
408
 
        y = a.y + (a.height-h)/2
409
 
        self.paint_star(cr, self, self.state, x, y, w, h)
410
 
        return
411
 
 
412
 
 
413
 
class SimpleStarRating(gtk.HBox, StarPainter):
414
 
 
415
 
    MAX_STARS = 5
416
 
    STAR_SIZE = (int(1.25*EM), int(1.25*EM))
417
 
 
418
 
    def __init__(self, n_stars=0, spacing=0, star_size=STAR_SIZE):
419
 
        gtk.HBox.__init__(self, spacing=spacing)
420
 
        StarPainter.__init__(self)
421
 
 
422
 
        self.n_stars = n_stars
423
 
        self.star_size = star_size
424
 
        self.set_max_stars(self.MAX_STARS)
425
 
 
426
 
        self.connect('expose-event', self._on_expose)
427
 
        return
428
 
 
429
 
    def _on_expose(self, widget, event):
430
 
        cr = widget.window.cairo_create()
431
 
        self.draw(cr, self.allocation)
432
 
        del cr
433
 
        return
434
 
 
435
 
    def _calc_size(self, max_stars):
436
 
        sw, sh = self.star_size
437
 
        w = max_stars*(sw+self.get_spacing())
438
 
        self.set_size_request(w, sh)
439
 
        return
440
 
 
441
 
    def draw(self, cr, a):
442
 
        sw = self.get_property('width-request')/self.MAX_STARS
443
 
        self.paint_rating(cr, self,
444
 
                          self.state,
445
 
                          a.x, a.y,
446
 
                          (sw, sw),
447
 
                          self.MAX_STARS,
448
 
                          self.n_stars)
449
 
        return
450
 
 
451
 
    def queue_draw(self):
452
 
        a = self.allocation
453
 
        self.queue_draw_area(a.x-5, a.y-5, a.width+10, a.height+10)
454
 
        return
455
 
 
456
 
    def set_max_stars(self, max_stars):
457
 
        self.max_stars = max_stars
458
 
        self._calc_size(max_stars)
459
 
        return
460
 
 
461
 
    def set_rating(self, rating):
462
 
        if rating is None: rating = 0
463
 
        self.n_stars = rating
464
 
        self.queue_draw()
465
 
        return
466
 
 
467
 
 
468
 
class StarRating(gtk.Alignment):
469
 
 
470
 
    MAX_STARS = 5
471
 
 
472
 
    __gsignals__ = {'changed':(gobject.SIGNAL_RUN_LAST,
473
 
                               gobject.TYPE_NONE,
474
 
                               ())
475
 
                    }
476
 
 
477
 
 
478
 
    def __init__(self, n_stars=0, spacing=0, star_size=(EM,EM), is_interactive=False):
479
 
        gtk.Alignment.__init__(self, 0.5, 0.5)
480
 
 
481
 
        self.set_padding(2, 2, 0, 0)
482
 
        self.hbox = gtk.HBox(spacing=spacing)
483
 
        self.add(self.hbox)
484
 
 
485
 
        self.rating = 0
486
 
 
487
 
        self._build(star_size, is_interactive)
488
 
        if n_stars != None:
489
 
            self.set_rating(n_stars)
490
 
 
491
 
    def _build(self, star_size, is_interactive):
492
 
        for i in range(self.MAX_STARS):
493
 
            star = StarWidget(star_size, is_interactive)
494
 
            star.position = i
495
 
            self.hbox.pack_start(star, expand=False)
496
 
        self.show_all()
497
 
 
498
 
    def set_paint_style(self, paint_style):
499
 
        for star in self.get_stars():
500
 
            star.set_paint_style(paint_style)
501
 
        return
502
 
 
503
 
    def set_rating(self, n_stars):
504
 
        if n_stars is None: n_stars = 0
505
 
        self.rating = n_stars
506
 
 
507
 
        acc = self.get_accessible()
508
 
        acc.set_name(_("%s star rating") % n_stars)
509
 
        acc.set_description(_("%s star rating") % n_stars)
510
 
 
511
 
        for i, star in enumerate(self.get_stars()):
512
 
            if i < int(n_stars):
513
 
                star.set_fill(StarPainter.FILL_FULL)
514
 
            elif i == int(n_stars) and n_stars-int(n_stars) > 0:
515
 
                star.set_fill(StarPainter.FILL_HALF)
516
 
            else:
517
 
                star.set_fill(StarPainter.FILL_EMPTY)
518
 
 
519
 
        self.emit('changed')
520
 
        self.queue_draw()
521
 
        return
522
 
 
523
 
    def get_rating(self):
524
 
        return self.rating
525
 
 
526
 
    def get_stars(self):
527
 
        return filter(lambda x: isinstance(x, StarWidget), self.hbox.get_children())
528
 
 
529
 
 
530
 
class StarRatingSelector(StarRating):
531
 
 
532
 
    RATING_WORDS = [_('Hint: Click a star to rate this app'),   # unrated caption
533
 
                    _('Awful'),         # 1 star rating
534
 
                    _('Poor'),          # 2 star rating
535
 
                    _('Adequate'),      # 3 star rating
536
 
                    _('Good'),          # 4 star rating
537
 
                    _('Excellent')]     # 5 star rating
538
 
 
539
 
 
540
 
    def __init__(self, n_stars=None, spacing=1, star_size=(4*EM,4*EM)):
541
 
        StarRating.__init__(self, n_stars, spacing, star_size, True)
542
 
 
543
 
        for star in self.get_stars():
544
 
            self._connect_signals(star)
545
 
 
546
 
        self.set_paint_style(StarPainter.STYLE_INTERACTIVE)
547
 
 
548
 
        self.caption = None
549
 
        return
550
 
 
551
 
    def _on_enter(self, star, event):
552
 
        star.set_state(gtk.STATE_PRELIGHT)
553
 
        self.set_tentative_rating(star.position+1)
554
 
        if self.caption:
555
 
            self.caption.set_markup(_(self.RATING_WORDS[star.position+1]))
556
 
        return
557
 
 
558
 
    def _on_leave(self, star, event):
559
 
        star.set_state(gtk.STATE_NORMAL)
560
 
        gobject.timeout_add(100, self._hover_check_cb)
561
 
        star.queue_draw()
562
 
        return
563
 
 
564
 
    def _on_press(self, star, event):
565
 
        a_star_has_focus = filter(lambda s: s.has_focus(),
566
 
                                  self.get_stars())
567
 
        if a_star_has_focus: star.grab_focus()
568
 
 
569
 
        star.set_state(gtk.STATE_ACTIVE)
570
 
        star.queue_draw()
571
 
        return
572
 
 
573
 
    def _on_release(self, star, event):
574
 
        self.set_rating(star.position+1)
575
 
        star.set_state(gtk.STATE_PRELIGHT)
576
 
        star.queue_draw()
577
 
        return
578
 
 
579
 
    def _on_focus_in(self, star, event):
580
 
        self.set_tentative_rating(star.position+1)
581
 
        return True
582
 
 
583
 
    def _on_focus_out(self, star, event):
584
 
        self.set_tentative_rating(0)
585
 
        return True
586
 
 
587
 
    def _on_key_press(self, star, event):
588
 
        kv = event.keyval
589
 
        if kv == gtk.keysyms.space or kv == gtk.keysyms.Return:
590
 
            star.set_state(gtk.STATE_ACTIVE)
591
 
            a = star.allocation
592
 
            star.queue_draw_area(a.x-2, a.y-2, a.width+4, a.height+4)
593
 
        elif kv == gtk.keysyms._1:
594
 
            self.set_rating(1)
595
 
        elif kv == gtk.keysyms._2:
596
 
            self.set_rating(2)
597
 
        elif kv == gtk.keysyms._3:
598
 
            self.set_rating(3)
599
 
        elif kv == gtk.keysyms._4:
600
 
            self.set_rating(4)
601
 
        elif kv == gtk.keysyms._5:
602
 
            self.set_rating(5)
603
 
        if self.caption:
604
 
            self.caption.set_markup(_(self.RATING_WORDS[self.rating]))
605
 
        return
606
 
 
607
 
    def _on_key_release(self, star, event):
608
 
        kv = event.keyval
609
 
        if kv == gtk.keysyms.space or kv == gtk.keysyms.Return:
610
 
            self.set_rating(star.position+1)
611
 
            star.set_state(gtk.STATE_NORMAL)
612
 
            star.queue_draw()
613
 
        return
614
 
 
615
 
    def _connect_signals(self, star):
616
 
        star.connect('enter-notify-event', self._on_enter)
617
 
        star.connect('leave-notify-event', self._on_leave)
618
 
        star.connect('button-press-event', self._on_press)
619
 
        star.connect('button-release-event', self._on_release)
620
 
        star.connect('focus-in-event', self._on_focus_in)
621
 
        star.connect('focus-out-event', self._on_focus_out)
622
 
        star.connect('key-press-event', self._on_key_press)
623
 
        star.connect('key-release-event', self._on_key_release)
624
 
        return
625
 
 
626
 
    def _hover_check_cb(self):
627
 
        x, y, flags = self.window.get_pointer()
628
 
        if not gtk.gdk.region_rectangle(self.hbox.allocation).point_in(x,y):
629
 
            self.set_tentative_rating(0)
630
 
            if self.caption:
631
 
                self.caption.set_markup(_(self.RATING_WORDS[self.rating]))
632
 
        return
633
 
 
634
 
    def queue_draw(self):
635
 
        a = self.allocation
636
 
        self.queue_draw_area(a.x-5, a.y-5, a.width+10, a.height+10)
637
 
        return
638
 
 
639
 
    def set_caption_widget(self, caption_widget):
640
 
        caption_widget.set_markup(_(self.RATING_WORDS[0]))
641
 
        self.caption = caption_widget
642
 
        return
643
 
 
644
 
    def set_tentative_rating(self, n_stars):
645
 
        for i, star in enumerate(self.get_stars()):
646
 
            if i < int(n_stars):
647
 
                star.border = StarPainter.BORDER_ON
648
 
            else:
649
 
                star.border = StarPainter.BORDER_OFF
650
 
        self.queue_draw()
651
 
        return
652
 
 
653
 
 
654
 
class StarCaption(gtk.Label):
655
 
 
656
 
    def __init__(self):
657
 
        gtk.Label.__init__(self)
658
 
        #self.shape = ShapeRoundedRectangle()
659
 
        #self.connect('expose-event', self._on_expose)
660
 
        return
661
 
 
662
 
    #def _on_expose(self, widget, event):
663
 
        #a = widget.allocation
664
 
        #cr = widget.window.cairo_create()
665
 
        #self.shape.layout(cr, a.x, a.y, a.x+a.width, a.y+a.height, radius=3)
666
 
        #light = self.style.light[0]
667
 
        #cr.set_source_rgb(light.red_float, light.green_float, light.blue_float)
668
 
        #cr.fill()
669
 
        #del cr
670
 
        #return
671
 
 
672
 
    def set_markup(self, markup):
673
 
        gtk.Label.set_markup(self, '<small>%s</small>' % markup)
674
 
        return
675
 
 
676
 
 
677
 
class ReviewStatsContainer(gtk.VBox):
678
 
 
679
 
    SIZE = (3*EM, 3*EM)
680
 
 
681
 
    def __init__(self):
682
 
        gtk.VBox.__init__(self, spacing=4)
683
 
        self.star_rating = StarRating(star_size=self.SIZE)
684
 
        self.star_rating.set_paint_style(StarPainter.STYLE_BIG)
685
 
        self.label = gtk.Label()
686
 
        self.pack_start(self.star_rating, False)
687
 
        self.pack_start(self.label, False, False)
688
 
 
689
 
    def set_avg_rating(self, avg_rating):
690
 
        self.star_rating.set_rating(avg_rating)
691
 
 
692
 
    def set_nr_reviews(self, nr_reviews):
693
 
        self.nr_reviews = nr_reviews
694
 
        self._update_nr_reviews()
695
 
 
696
 
    # internal stuff
697
 
    def _update_nr_reviews(self):
698
 
        s = gettext.ngettext(
699
 
            "%(nr_ratings)i rating",
700
 
            "%(nr_ratings)i ratings",
701
 
            self.nr_reviews) % { 'nr_ratings' : self.nr_reviews, }
702
 
        self.label.set_markup(s)
703
 
 
704
 
 
705
 
class UIReviewsList(gtk.VBox):
706
 
 
707
 
    __gsignals__ = {
708
 
        'new-review':(gobject.SIGNAL_RUN_FIRST,
709
 
                    gobject.TYPE_NONE,
710
 
                    ()),
711
 
        'report-abuse':(gobject.SIGNAL_RUN_FIRST,
712
 
                    gobject.TYPE_NONE,
713
 
                    (gobject.TYPE_PYOBJECT,)),
714
 
        'submit-usefulness':(gobject.SIGNAL_RUN_FIRST,
715
 
                    gobject.TYPE_NONE,
716
 
                    (gobject.TYPE_PYOBJECT, bool)),
717
 
        'modify-review':(gobject.SIGNAL_RUN_FIRST,
718
 
                    gobject.TYPE_NONE,
719
 
                    (gobject.TYPE_PYOBJECT,)),
720
 
        'delete-review':(gobject.SIGNAL_RUN_FIRST,
721
 
                    gobject.TYPE_NONE,
722
 
                    (gobject.TYPE_PYOBJECT,)),
723
 
        'more-reviews-clicked':(gobject.SIGNAL_RUN_FIRST,
724
 
                                gobject.TYPE_NONE,
725
 
                                () ),
726
 
        'different-review-language-clicked':(gobject.SIGNAL_RUN_FIRST,
727
 
                                             gobject.TYPE_NONE,
728
 
                                             (gobject.TYPE_STRING,) ),
729
 
    }
730
 
 
731
 
    def __init__(self, parent):
732
 
        gtk.VBox.__init__(self)
733
 
        self.logged_in_person = get_person_from_config()
734
 
 
735
 
        self._parent = parent
736
 
        # this is a list of review data (softwarecenter.backend.reviews.Review)
737
 
        self.reviews = []
738
 
        # global review stats, this includes ratings in different languages
739
 
        self.global_review_stats = None
740
 
        # usefulness stuff
741
 
        self.useful_votes = UsefulnessCache()
742
 
        self.logged_in_person = None
743
 
 
744
 
        label = mkit.EtchedLabel(_("Reviews"))
745
 
        label.set_padding(6, 6)
746
 
        label.set_use_markup(True)
747
 
        label.set_alignment(0, 0.5)
748
 
 
749
 
        self.new_review = mkit.VLinkButton(_("Write your own review"))
750
 
        self.new_review.set_underline(True)
751
 
 
752
 
        self.header = hb = gtk.HBox(spacing=mkit.SPACING_MED)
753
 
        self.pack_start(hb, False)
754
 
        hb.pack_start(label, False)
755
 
        hb.pack_end(self.new_review, False, padding=6)
756
 
 
757
 
        self.vbox = gtk.VBox(spacing=24)
758
 
        self.vbox.set_border_width(6)
759
 
        self.pack_start(self.vbox)
760
 
 
761
 
        self.new_review.connect('clicked', lambda w: self.emit('new-review'))
762
 
        self.show_all()
763
 
        return
764
 
 
765
 
    def _on_button_new_clicked(self, button):
766
 
        self.emit("new-review")
767
 
    
768
 
    def update_useful_votes(self, my_votes):
769
 
        self.useful_votes = my_votes
770
 
 
771
 
    def _fill(self):
772
 
        """ take the review data object from self.reviews and build the
773
 
            UI vbox out of them
774
 
        """
775
 
        self.logged_in_person = get_person_from_config()
776
 
        if self.reviews:
777
 
            for r in self.reviews:
778
 
                pkgversion = self._parent.app_details.version
779
 
                review = UIReview(r, pkgversion, self.logged_in_person, self.useful_votes)
780
 
                self.vbox.pack_start(review)
781
 
        return
782
 
 
783
 
    def _be_the_first_to_review(self):
784
 
        s = _('Be the first to review it')
785
 
        self.new_review.set_label(s)
786
 
        self.vbox.pack_start(NoReviewYetWriteOne())
787
 
        self.vbox.show_all()
788
 
        return
789
 
 
790
 
    def _install_to_review(self):
791
 
        s = '<small><b>%s</b></small>' % _("You need to install this app before you can review it")
792
 
        self.install_first_label = gtk.Label(s)
793
 
        self.install_first_label.set_use_markup(True)
794
 
        self.header.pack_end(self.install_first_label, False, padding=2)
795
 
        self.install_first_label.show()
796
 
        return
797
 
    
798
 
    # FIXME: this needs to be smarter in the future as we will
799
 
    #        not allow multiple reviews for the same software version
800
 
    def _any_reviews_current_user(self):
801
 
        for review in self.reviews:
802
 
            if self.logged_in_person == review.reviewer_username:
803
 
                return True
804
 
        return False
805
 
 
806
 
    def _add_no_network_connection_msg(self):
807
 
        title = _('No Network Connection')
808
 
        msg = _('Only saved reviews can be displayed')
809
 
        m = EmbeddedMessage(title, msg, 'network-offline')
810
 
        self.vbox.pack_start(m)
811
 
        
812
 
    def _clear_vbox(self, vbox):
813
 
        children = vbox.get_children()
814
 
        for child in children:
815
 
            child.destroy()
816
 
 
817
 
    # FIXME: instead of clear/add_reviews/configure_reviews_ui we should provide
818
 
    #        a single show_reviews(reviews_data_list)
819
 
    def configure_reviews_ui(self):
820
 
        """ this needs to be called after add_reviews, it will actually
821
 
            show the reviews
822
 
        """
823
 
        #print 'Review count: %s' % len(self.reviews)
824
 
 
825
 
        try:
826
 
            self.install_first_label.hide()
827
 
        except AttributeError:
828
 
            pass
829
 
        
830
 
        self._clear_vbox(self.vbox)
831
 
 
832
 
        # network sensitive stuff, only show write_review if connected,
833
 
        # add msg about offline cache usage if offline
834
 
        is_connected = network_state_is_connected()
835
 
        self.new_review.set_sensitive(is_connected)
836
 
        if not is_connected:
837
 
            self._add_no_network_connection_msg()
838
 
 
839
 
        # only show new_review for installed stuff
840
 
        is_installed = (self._parent.app_details and
841
 
                        self._parent.app_details.pkg_state == PkgStates.INSTALLED)
842
 
        if is_installed:
843
 
            self.new_review.show()
844
 
        else:
845
 
            self.new_review.hide()
846
 
            self._install_to_review()
847
 
 
848
 
        # always hide spinner and call _fill (fine if there is nothing to do)
849
 
        self.hide_spinner()
850
 
        self._fill()
851
 
        self.vbox.show_all()
852
 
 
853
 
        if self.reviews:
854
 
            # adjust label if we have reviews
855
 
            if self._any_reviews_current_user():
856
 
                self.new_review.hide()
857
 
            else:
858
 
                self.new_review.set_label(_("Write your own review"))
859
 
        else:
860
 
            # no reviews, either offer to write one or show "none"
861
 
            if is_installed and is_connected:
862
 
                self._be_the_first_to_review()
863
 
            else:
864
 
                self.vbox.pack_start(NoReviewYet())
865
 
 
866
 
        # if there are no reviews, try english as fallback
867
 
        language = get_language()
868
 
        if (len(self.reviews) == 0 and
869
 
            self.global_review_stats and
870
 
            self.global_review_stats.ratings_total > 0 and
871
 
            language != "en"):
872
 
            button = gtk.Button(_("Show reviews in english"))
873
 
            button.connect(
874
 
                "clicked", self._on_different_review_language_clicked)
875
 
            button.show()
876
 
            self.vbox.pack_start(button)                
877
 
 
878
 
        # aaronp: removed check to see if the length of reviews is divisible by
879
 
        # the batch size to allow proper fixing of LP: #794060 as when a review
880
 
        # is submitted and appears in the list, the pagination will break this
881
 
        # check and make it unreliable
882
 
        # if self.reviews and len(self.reviews) % REVIEWS_BATCH_PAGE_SIZE == 0:
883
 
        if self.reviews:
884
 
            button = gtk.Button(_("Check for more reviews"))
885
 
            button.connect("clicked", self._on_more_reviews_clicked)
886
 
            button.show()
887
 
            self.vbox.pack_start(button)                
888
 
        return
889
 
 
890
 
    def _on_more_reviews_clicked(self, button):
891
 
        # remove buttn and emit signal
892
 
        self.vbox.remove(button)
893
 
        self.emit("more-reviews-clicked")
894
 
 
895
 
    def _on_different_review_language_clicked(self, button):
896
 
        language = "en"
897
 
        self.vbox.remove(button)
898
 
        self.emit("different-review-language-clicked", language)
899
 
 
900
 
    def add_review(self, review):
901
 
        self.reviews.append(review)
902
 
        return
903
 
        
904
 
    def replace_review(self, review):
905
 
        for r in self.reviews:
906
 
            if r.id == review.id:
907
 
                pos = self.reviews.index(r)
908
 
                self.reviews.remove(r)
909
 
                self.reviews.insert(pos, review)
910
 
                break
911
 
        return
912
 
 
913
 
    def remove_review(self, review):
914
 
        for r in self.reviews:
915
 
            if r.id == review.id:
916
 
                self.reviews.remove(r)
917
 
                break
918
 
        return
919
 
 
920
 
    def get_all_review_ids(self):
921
 
        ids = []
922
 
        for review in self.reviews:
923
 
            ids.append(review.id)
924
 
        return ids 
925
 
 
926
 
    def clear(self):
927
 
        self.reviews = []
928
 
        for review in self.vbox:
929
 
            review.destroy()
930
 
        self.new_review.hide()
931
 
 
932
 
    # FIXME: ideally we would have "{show,hide}_loading_notice()" to
933
 
    #        easily allow changing from e.g. spinner to text
934
 
    def show_spinner_with_message(self, message):
935
 
        try:
936
 
            self.install_first_label.hide()
937
 
        except AttributeError:
938
 
            pass
939
 
        
940
 
        a = gtk.Alignment(0.5, 0.5)
941
 
 
942
 
        hb = gtk.HBox(spacing=12)
943
 
        a.add(hb)
944
 
 
945
 
        spinner = gtk.Spinner()
946
 
        spinner.start()
947
 
 
948
 
        hb.pack_start(spinner, False)
949
 
 
950
 
        l = mkit.EtchedLabel(message)
951
 
        l.set_use_markup(True)
952
 
 
953
 
        hb.pack_start(l, False)
954
 
 
955
 
        self.vbox.pack_start(a, False)
956
 
        self.vbox.show_all()
957
 
        return
958
 
    def hide_spinner(self):
959
 
        for child in self.vbox.get_children():
960
 
            if isinstance(child, gtk.Alignment):
961
 
                child.destroy()
962
 
        return
963
 
 
964
 
    def draw(self, cr, a):
965
 
        for r in self.vbox:
966
 
            if isinstance(r, (UIReview)):
967
 
                r.draw(cr, r.allocation)
968
 
        return
969
 
 
970
 
    # mvo: this appears to be not used
971
 
    #def get_reviews(self):
972
 
    #    return filter(lambda r: type(r) == UIReview, self.vbox.get_children())
973
 
 
974
 
class UIReview(gtk.VBox):
975
 
    """ the UI for a individual review including all button to mark
976
 
        useful/inappropriate etc
977
 
    """
978
 
    def __init__(self, review_data=None, app_version=None, 
979
 
                 logged_in_person=None, useful_votes=None):
980
 
        gtk.VBox.__init__(self, spacing=mkit.SPACING_LARGE)
981
 
 
982
 
        self.header = gtk.HBox(spacing=mkit.SPACING_MED)
983
 
        self.body = gtk.VBox()
984
 
        self.footer_split = gtk.VBox()
985
 
        self.footer = gtk.HBox()
986
 
 
987
 
        self.useful = None
988
 
        self.yes_like = None
989
 
        self.no_like = None
990
 
        self.status_box = gtk.HBox()
991
 
        self.delete_status_box = gtk.HBox()
992
 
        self.delete_error_img = gtk.Image()
993
 
        self.delete_error_img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_SMALL_TOOLBAR)        
994
 
        self.submit_error_img = gtk.Image()
995
 
        self.submit_error_img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_SMALL_TOOLBAR)
996
 
        self.submit_status_spinner = gtk.Spinner()
997
 
        self.submit_status_spinner.set_size_request(12,12)
998
 
        self.delete_status_spinner = gtk.Spinner()
999
 
        self.delete_status_spinner.set_size_request(12,12)
1000
 
        self.acknowledge_error = mkit.VLinkButton(_("<small>OK</small>"))
1001
 
        self.acknowledge_error.set_underline(True)
1002
 
        self.acknowledge_error.set_subdued(True)
1003
 
        self.delete_acknowledge_error = mkit.VLinkButton(_("<small>OK</small>"))
1004
 
        self.delete_acknowledge_error.set_underline(True)
1005
 
        self.delete_acknowledge_error.set_subdued(True)
1006
 
        self.usefulness_error = False
1007
 
        self.delete_error = False
1008
 
        self.modify_error = False
1009
 
 
1010
 
        self.pack_start(self.header, False)
1011
 
        self.pack_start(self.body, False)
1012
 
        self.pack_start(self.footer_split, False)
1013
 
        
1014
 
        self.logged_in_person = logged_in_person
1015
 
        self.person = None
1016
 
        self.id = None
1017
 
        self.useful_votes = useful_votes
1018
 
 
1019
 
        self._allocation = None
1020
 
 
1021
 
        if review_data:
1022
 
            self.connect('realize',
1023
 
                         self._on_realize,
1024
 
                         review_data,
1025
 
                         app_version,
1026
 
                         logged_in_person,
1027
 
                         useful_votes)
1028
 
        return
1029
 
 
1030
 
    def _on_realize(self, w, review_data, app_version, logged_in_person, useful_votes):
1031
 
        self._build(review_data, app_version, logged_in_person, useful_votes)
1032
 
        return
1033
 
 
1034
 
    def _on_allocate(self, widget, allocation, stars, summary, text, who_when, version_lbl, flag):
1035
 
        if self._allocation == allocation:
1036
 
            LOG_ALLOCATION.debug("UIReviewAllocate skipped!")
1037
 
            return True
1038
 
        self._allocation = allocation
1039
 
 
1040
 
        summary.set_size_request(max(20, allocation.width - \
1041
 
                                 stars.allocation.width - \
1042
 
                                 who_when.allocation.width - 20), -1)
1043
 
 
1044
 
        text.set_size_request(allocation.width, -1)
1045
 
 
1046
 
        if version_lbl:
1047
 
            version_lbl.set_size_request(allocation.width-flag.allocation.width-20, -1)
1048
 
        return
1049
 
 
1050
 
    def _on_report_abuse_clicked(self, button):
1051
 
        reviews = self.get_ancestor(UIReviewsList)
1052
 
        if reviews:
1053
 
            reviews.emit("report-abuse", self.id)
1054
 
    
1055
 
    def _on_modify_clicked(self, button):
1056
 
        reviews = self.get_ancestor(UIReviewsList)
1057
 
        if reviews:
1058
 
            reviews.emit("modify-review", self.id)
1059
 
    
1060
 
    def _on_useful_clicked(self, btn, is_useful):
1061
 
        reviews = self.get_ancestor(UIReviewsList)
1062
 
        if reviews:
1063
 
            self._usefulness_ui_update('progress')
1064
 
            reviews.emit("submit-usefulness", self.id, is_useful)
1065
 
    
1066
 
    def _on_error_acknowledged(self, button, current_user_reviewer, useful_total, useful_favorable):
1067
 
        self.usefulness_error = False
1068
 
        self._usefulness_ui_update('renew', current_user_reviewer, useful_total, useful_favorable)
1069
 
    
1070
 
    def _usefulness_ui_update(self, type, current_user_reviewer=False, useful_total=0, useful_favorable=0):
1071
 
        self._hide_usefulness_elements()
1072
 
        #print "_usefulness_ui_update: %s" % type
1073
 
        if type == 'renew':
1074
 
            self._build_usefulness_ui(current_user_reviewer, useful_total, useful_favorable, self.useful_votes)
1075
 
            return
1076
 
        if type == 'progress':
1077
 
            self.submit_status_spinner.start()
1078
 
            self.submit_status_spinner.show()
1079
 
            self.status_label = gtk.Label("<small><b>%s</b></small>" % _(u"Submitting now\u2026"))
1080
 
            self.status_box.pack_start(self.submit_status_spinner, False)
1081
 
            self.status_label.set_use_markup(True)
1082
 
            self.status_label.set_padding(2,0)
1083
 
            self.status_box.pack_start(self.status_label,False)
1084
 
            self.status_label.show()
1085
 
        if type == 'error':
1086
 
            self.submit_error_img.show()
1087
 
            self.status_label = gtk.Label("<small><b>%s</b></small>" % _("Error submitting usefulness"))
1088
 
            self.status_box.pack_start(self.submit_error_img, False)
1089
 
            self.status_label.set_use_markup(True)
1090
 
            self.status_label.set_padding(2,0)
1091
 
            self.status_box.pack_start(self.status_label,False)
1092
 
            self.status_label.show()
1093
 
            self.acknowledge_error.show()
1094
 
            self.status_box.pack_start(self.acknowledge_error,False)
1095
 
            self.acknowledge_error.connect('clicked', self._on_error_acknowledged, current_user_reviewer, useful_total, useful_favorable)
1096
 
        self.status_box.show()
1097
 
        self.footer.pack_start(self.status_box, False)
1098
 
        return
1099
 
 
1100
 
    def _hide_usefulness_elements(self):
1101
 
        """ hide all usefulness elements """
1102
 
        for attr in ["useful", "yes_like", "no_like", "submit_status_spinner",
1103
 
                     "submit_error_img", "status_box", "status_label",
1104
 
                     "acknowledge_error", "yes_no_separator"
1105
 
                     ]:
1106
 
            widget = getattr(self, attr, None)
1107
 
            if widget:
1108
 
                widget.hide()
1109
 
        return
1110
 
 
1111
 
    def _get_datetime_from_review_date(self, raw_date_str):
1112
 
        # example raw_date str format: 2011-01-28 19:15:21
1113
 
        return datetime.datetime.strptime(raw_date_str, '%Y-%m-%d %H:%M:%S')
1114
 
 
1115
 
    def _delete_ui_update(self, type, current_user_reviewer=False, action=None):
1116
 
        self._hide_delete_elements()
1117
 
        if type == 'renew':
1118
 
            self._build_delete_flag_ui(current_user_reviewer)
1119
 
            return
1120
 
        if type == 'progress':
1121
 
            self.delete_status_spinner.start()
1122
 
            self.delete_status_spinner.show()
1123
 
            self.delete_status_label = gtk.Label("<small><b>%s</b></small>" % _(u"Deleting now\u2026"))
1124
 
            self.delete_status_box.pack_start(self.delete_status_spinner, False)
1125
 
            self.delete_status_label.set_use_markup(True)
1126
 
            self.delete_status_label.set_padding(2,0)
1127
 
            self.delete_status_box.pack_start(self.delete_status_label,False)
1128
 
            self.delete_status_label.show()
1129
 
        if type == 'error':
1130
 
            self.delete_error_img.show()
1131
 
            self.delete_status_label = gtk.Label("<small><b>%s</b></small>" % _("Error %s review" % action))
1132
 
            self.delete_status_box.pack_start(self.delete_error_img, False)
1133
 
            self.delete_status_label.set_use_markup(True)
1134
 
            self.delete_status_label.set_padding(2,0)
1135
 
            self.delete_status_box.pack_start(self.delete_status_label,False)
1136
 
            self.delete_status_label.show()
1137
 
            self.delete_acknowledge_error.show()
1138
 
            self.delete_status_box.pack_start(self.delete_acknowledge_error,False)
1139
 
            self.delete_acknowledge_error.connect('clicked', self._on_delete_error_acknowledged, current_user_reviewer)
1140
 
        self.delete_status_box.show()
1141
 
        self.footer.pack_end(self.delete_status_box, False)
1142
 
        return
1143
 
    
1144
 
    def _on_delete_clicked(self, btn):
1145
 
        reviews = self.get_ancestor(UIReviewsList)
1146
 
        if reviews:
1147
 
            self._delete_ui_update('progress')
1148
 
            reviews.emit("delete-review", self.id)
1149
 
    
1150
 
    def _on_delete_error_acknowledged(self, button, current_user_reviewer):
1151
 
        self.delete_error = False
1152
 
        self._delete_ui_update('renew', current_user_reviewer)
1153
 
        
1154
 
    def _hide_delete_elements(self):
1155
 
        """ hide all delete elements """
1156
 
        for attr in ["complain", "edit", "delete", "delete_status_spinner",
1157
 
                     "delete_error_img", "delete_status_box", "delete_status_label",
1158
 
                     "delete_acknowledge_error", "flagbox"
1159
 
                     ]:
1160
 
            o = getattr(self, attr, None)
1161
 
            if o:
1162
 
                o.hide()
1163
 
        return
1164
 
    
1165
 
    def _build(self, review_data, app_version, logged_in_person, useful_votes):
1166
 
 
1167
 
        # all the attributes of review_data may need markup escape, 
1168
 
        # depening on if they are used as text or markup
1169
 
        self.id = review_data.id
1170
 
        self.person = review_data.reviewer_username
1171
 
        displayname = review_data.reviewer_displayname
1172
 
        # example raw_date str format: 2011-01-28 19:15:21
1173
 
        cur_t = self._get_datetime_from_review_date(review_data.date_created)
1174
 
 
1175
 
        app_name = review_data.app_name or review_data.package_name
1176
 
        review_version = review_data.version
1177
 
        self.useful_total = useful_total = review_data.usefulness_total
1178
 
        useful_favorable = review_data.usefulness_favorable
1179
 
        useful_submit_error = review_data.usefulness_submit_error
1180
 
        delete_error = review_data.delete_error
1181
 
        modify_error = review_data.modify_error
1182
 
 
1183
 
        dark_color = self.style.dark[gtk.STATE_NORMAL]
1184
 
        m = self._whom_when_markup(self.person, displayname, cur_t, dark_color)
1185
 
 
1186
 
        who_when = mkit.EtchedLabel(m)
1187
 
        who_when.set_use_markup(True)
1188
 
 
1189
 
        summary = mkit.EtchedLabel('<b>%s</b>' % gobject.markup_escape_text(review_data.summary))
1190
 
        summary.set_use_markup(True)
1191
 
        summary.set_ellipsize(pango.ELLIPSIZE_END)
1192
 
        summary.set_selectable(True)
1193
 
        summary.set_alignment(0, 0.5)
1194
 
 
1195
 
        text = gtk.Label(review_data.review_text)
1196
 
        text.set_line_wrap(True)
1197
 
        text.set_selectable(True)
1198
 
        text.set_alignment(0, 0)
1199
 
 
1200
 
        stars = SimpleStarRating(review_data.rating)
1201
 
 
1202
 
        self.header.pack_start(stars, False)
1203
 
        self.header.pack_start(summary, False)
1204
 
        self.header.pack_end(who_when, False)
1205
 
        self.body.pack_start(text, False)
1206
 
        
1207
 
        #if review version is different to version of app being displayed, 
1208
 
        # alert user
1209
 
        version_lbl = None
1210
 
        if (review_version and
1211
 
            app_version and
1212
 
            upstream_version_compare(review_version, app_version) != 0):
1213
 
            version_string = _("This review was written for a different version of %(app_name)s (Version: %(version)s)") % { 
1214
 
                'app_name' : app_name,
1215
 
                'version' : gobject.markup_escape_text(upstream_version(review_version))
1216
 
                }
1217
 
 
1218
 
            m = '<small><i><span color="%s">%s</span></i></small>'
1219
 
            version_lbl = gtk.Label(m % (dark_color.to_string(), version_string))
1220
 
            version_lbl.set_use_markup(True)
1221
 
            version_lbl.set_padding(0,3)
1222
 
            version_lbl.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
1223
 
            version_lbl.set_alignment(0, 0.5)
1224
 
            self.footer_split.pack_start(version_lbl, False)
1225
 
 
1226
 
        self.footer_split.pack_start(self.footer, False)
1227
 
 
1228
 
        current_user_reviewer = False
1229
 
        if self.person == self.logged_in_person:
1230
 
            current_user_reviewer = True
1231
 
 
1232
 
        self._build_usefulness_ui(current_user_reviewer, useful_total,
1233
 
                                  useful_favorable, useful_votes, useful_submit_error)
1234
 
            
1235
 
        self.flagbox = gtk.HBox()
1236
 
        self._build_delete_flag_ui(current_user_reviewer, delete_error, modify_error)
1237
 
        self.footer.pack_end(self.flagbox,False)
1238
 
        self.body.connect('size-allocate', self._on_allocate, stars, summary, text, who_when, version_lbl, self.flagbox)
1239
 
            
1240
 
        return
1241
 
    
1242
 
    def _build_usefulness_ui(self, current_user_reviewer, useful_total, 
1243
 
                             useful_favorable, useful_votes, usefulness_submit_error=False):
1244
 
        if usefulness_submit_error:
1245
 
            self._usefulness_ui_update('error', current_user_reviewer, 
1246
 
                                       useful_total, useful_favorable)
1247
 
        else:
1248
 
            already_voted = useful_votes.check_for_usefulness(self.id)
1249
 
            #get correct label based on retrieved usefulness totals and 
1250
 
            # if user is reviewer
1251
 
            self.useful = self._get_usefulness_label(
1252
 
                current_user_reviewer, useful_total, useful_favorable, already_voted)
1253
 
            self.useful.set_use_markup(True)
1254
 
            #vertically centre so it lines up with the Yes and No buttons
1255
 
            self.useful.set_alignment(0, 0.5)
1256
 
 
1257
 
            self.useful.show()
1258
 
            self.footer.pack_start(self.useful, False, padding=3)
1259
 
            # add here, but only populate if its not the own review
1260
 
            self.likebox = gtk.HBox()
1261
 
            if already_voted == None and not current_user_reviewer:
1262
 
                self.yes_like = mkit.VLinkButton('<small>%s</small>' % _('Yes'))
1263
 
                self.no_like = mkit.VLinkButton('<small>%s</small>' % _('No'))
1264
 
                self.yes_like.set_underline(True)
1265
 
                self.no_like.set_underline(True)
1266
 
                self.yes_like.connect('clicked', self._on_useful_clicked, True)
1267
 
                self.no_like.connect('clicked', self._on_useful_clicked, False)
1268
 
                self.yes_no_separator = gtk.Label("<small>/</small>")
1269
 
                self.yes_no_separator.set_use_markup(True)
1270
 
                
1271
 
                self.yes_like.show()
1272
 
                self.no_like.show()
1273
 
                self.yes_no_separator.show()
1274
 
                self.likebox.set_spacing(3)
1275
 
                self.likebox.pack_start(self.yes_like, False)
1276
 
                self.likebox.pack_start(self.yes_no_separator, False)
1277
 
                self.likebox.pack_start(self.no_like, False)
1278
 
                self.footer.pack_start(self.likebox, False)
1279
 
            # always update network status (to keep the code simple)
1280
 
            self._update_likebox_based_on_network_state()
1281
 
        return
1282
 
 
1283
 
    def _update_likebox_based_on_network_state(self):
1284
 
        """ show/hide yes/no based on network connection state """
1285
 
        # FIXME: make this dynamic shode/hide on network changes
1286
 
        # FIXME2: make ti actually work, later show_all() kill it
1287
 
        #         currently
1288
 
        if network_state_is_connected():
1289
 
            self.likebox.show()
1290
 
            self.useful.show()
1291
 
        else:
1292
 
            self.likebox.hide()
1293
 
            # showing "was this useful is not interessting"
1294
 
            if self.useful_total == 0:
1295
 
                self.useful.hide()
1296
 
    
1297
 
    def _get_usefulness_label(self, current_user_reviewer, 
1298
 
                              useful_total,  useful_favorable, already_voted):
1299
 
        '''returns gtk.Label() to be used as usefulness label depending 
1300
 
           on passed in parameters
1301
 
        '''
1302
 
        if already_voted == None:
1303
 
            if useful_total == 0 and current_user_reviewer:
1304
 
                s = ""
1305
 
            elif useful_total == 0:
1306
 
                # no votes for the review yet
1307
 
                s = _("Was this review helpful?")
1308
 
            elif current_user_reviewer:
1309
 
                # user has already voted for the review
1310
 
                s = gettext.ngettext(
1311
 
                    "%(useful_favorable)s of %(useful_total)s people "
1312
 
                    "found this review helpful.",
1313
 
                    "%(useful_favorable)s of %(useful_total)s people "
1314
 
                    "found this review helpful.",
1315
 
                    useful_total) % { 'useful_total' : useful_total,
1316
 
                                    'useful_favorable' : useful_favorable,
1317
 
                                    }
1318
 
            else:
1319
 
                # user has not already voted for the review
1320
 
                s = gettext.ngettext(
1321
 
                    "%(useful_favorable)s of %(useful_total)s people "
1322
 
                    "found this review helpful. Did you?",
1323
 
                    "%(useful_favorable)s of %(useful_total)s people "
1324
 
                    "found this review helpful. Did you?",
1325
 
                    useful_total) % { 'useful_total' : useful_total,
1326
 
                                    'useful_favorable' : useful_favorable,
1327
 
                                    }
1328
 
        else:
1329
 
        #only display these special strings if the user voted either way
1330
 
            if already_voted:
1331
 
                if useful_total == 1:
1332
 
                    s = _("You found this review helpful.")
1333
 
                else:
1334
 
                    s = gettext.ngettext(
1335
 
                        "%(useful_favorable)s of %(useful_total)s people "
1336
 
                        "found this review helpful, including you",
1337
 
                        "%(useful_favorable)s of %(useful_total)s people "
1338
 
                        "found this review helpful, including you.",
1339
 
                        useful_total) % { 'useful_total' : useful_total,
1340
 
                                    'useful_favorable' : useful_favorable,
1341
 
                                    }
1342
 
            else:
1343
 
                if useful_total == 1:
1344
 
                    s = _("You found this review unhelpful.")
1345
 
                else:
1346
 
                    s = gettext.ngettext(
1347
 
                        "%(useful_favorable)s of %(useful_total)s people "
1348
 
                        "found this review helpful; you did not.",
1349
 
                        "%(useful_favorable)s of %(useful_total)s people "
1350
 
                        "found this review helpful; you did not.",
1351
 
                        useful_total) % { 'useful_total' : useful_total,
1352
 
                                    'useful_favorable' : useful_favorable,
1353
 
                                    }
1354
 
                    
1355
 
        
1356
 
        return gtk.Label('<small>%s</small>' % s)
1357
 
 
1358
 
    def _build_delete_flag_ui(self, current_user_reviewer, delete_error=False, modify_error=False):
1359
 
        if delete_error:
1360
 
            self._delete_ui_update('error', current_user_reviewer, 'deleting')
1361
 
        elif modify_error:
1362
 
            self._delete_ui_update('error', current_user_reviewer, 'modifying')
1363
 
        else:
1364
 
            if current_user_reviewer:
1365
 
                self.edit = mkit.VLinkButton('<small>%s</small>' %_('Edit'))
1366
 
                self.delete = mkit.VLinkButton('<small>%s</small>' %_('Delete'))
1367
 
                self.edit.set_underline(True)
1368
 
                self.delete.set_underline(True)
1369
 
                self.edit.set_subdued(True)
1370
 
                self.delete.set_subdued(True)
1371
 
                self.flagbox.pack_start(self.edit, False)
1372
 
                self.flagbox.pack_start(self.delete, False)
1373
 
                self.edit.connect('clicked', self._on_modify_clicked)
1374
 
                self.delete.connect('clicked', self._on_delete_clicked)
1375
 
            else:
1376
 
                # Translators: This link is for flagging a review as inappropriate.
1377
 
                # To minimize repetition, if at all possible, keep it to a single word.
1378
 
                # If your language has an obvious verb, it won't need a question mark.
1379
 
                self.complain = mkit.VLinkButton('<small>%s</small>' % _('Inappropriate?'))
1380
 
                self.complain.set_subdued(True)
1381
 
                self.complain.set_underline(True)
1382
 
                self.complain.set_sensitive(network_state_is_connected())
1383
 
                self.flagbox.pack_start(self.complain, False)
1384
 
                self.complain.connect('clicked', self._on_report_abuse_clicked)
1385
 
            self.flagbox.show_all()
1386
 
            return
1387
 
 
1388
 
    def _whom_when_markup(self, person, displayname, cur_t, dark_color):
1389
 
        nice_date = get_nice_date_string(cur_t)
1390
 
        #dt = datetime.datetime.utcnow() - cur_t
1391
 
 
1392
 
        # prefer displayname if available
1393
 
        correct_name = displayname or person
1394
 
 
1395
 
        if person == self.logged_in_person:
1396
 
            m = '<span color="%s"><b>%s (%s)</b>, %s</span>' % (
1397
 
                dark_color.to_string(),
1398
 
                gobject.markup_escape_text(correct_name),
1399
 
                # TRANSLATORS: displayed in a review after the persons name,
1400
 
                # e.g. "Wonderful text based app" mvo (that's you) 2011-02-11"
1401
 
                _("that's you"),
1402
 
                gobject.markup_escape_text(nice_date))
1403
 
        else:
1404
 
            m = '<span color="%s"><b>%s</b>, %s</span>' % (
1405
 
                dark_color.to_string(),
1406
 
                gobject.markup_escape_text(correct_name),
1407
 
                gobject.markup_escape_text(nice_date))
1408
 
 
1409
 
        return m
1410
 
 
1411
 
    def draw(self, cr, a):
1412
 
        cr.save()
1413
 
        if not self.person == self.logged_in_person:
1414
 
            return
1415
 
 
1416
 
        cr.rectangle(a)
1417
 
 
1418
 
        color = mkit.floats_from_gdkcolor(self.style.mid[self.state])
1419
 
        cr.set_source_rgba(*color+(0.2,))
1420
 
 
1421
 
        cr.fill()
1422
 
 
1423
 
        cr.restore()
1424
 
 
1425
 
class EmbeddedMessage(UIReview):
1426
 
 
1427
 
    def __init__(self, title='', message='', icon_name=''):
1428
 
        UIReview.__init__(self)
1429
 
        self.label = None
1430
 
        self.image = None
1431
 
        
1432
 
        a = gtk.Alignment(0.5, 0.5)
1433
 
        self.body.pack_start(a, False)
1434
 
 
1435
 
        hb = gtk.HBox(spacing=12)
1436
 
        a.add(hb)
1437
 
 
1438
 
        if icon_name:
1439
 
            self.image = gtk.image_new_from_icon_name(icon_name,
1440
 
                                                      gtk.ICON_SIZE_DIALOG)
1441
 
            hb.pack_start(self.image, False)
1442
 
 
1443
 
        self.label = gtk.Label()
1444
 
        self.label.set_line_wrap(True)
1445
 
        self.label.set_alignment(0, 0.5)
1446
 
 
1447
 
        if title:
1448
 
            self.label.set_markup('<b><big>%s</big></b>\n%s' % (title, message))
1449
 
        else:
1450
 
            self.label.set_markup(message)
1451
 
 
1452
 
        hb.pack_start(self.label)
1453
 
 
1454
 
        self.show_all()
1455
 
        return
1456
 
 
1457
 
    def draw(self, cr, a):
1458
 
        cr.save()
1459
 
        cr.rectangle(a)
1460
 
        color = mkit.floats_from_gdkcolor(self.style.mid[self.state])
1461
 
        cr.set_source_rgba(*color+(0.2,))
1462
 
        cr.fill()
1463
 
        cr.restore()
1464
 
 
1465
 
 
1466
 
class NoReviewYet(EmbeddedMessage):
1467
 
    """ represents if there are no reviews yet and the app is not installed """
1468
 
    def __init__(self, *args, **kwargs):
1469
 
        # TRANSLATORS: displayed if there are no reviews for the app yet
1470
 
        #              and the user does not have it installed
1471
 
        title = _("This app has not been reviewed yet")
1472
 
        msg = _('You need to install this app before you can review it')
1473
 
        EmbeddedMessage.__init__(self, title, msg)
1474
 
 
1475
 
 
1476
 
class NoReviewYetWriteOne(EmbeddedMessage):
1477
 
    """ represents if there are no reviews yet and the app is installed """
1478
 
    def __init__(self, *args, **kwargs):
1479
 
 
1480
 
        # TRANSLATORS: displayed if there are no reviews yet and the user
1481
 
        #              has the app installed
1482
 
        title = _('Got an opinion?')
1483
 
        msg = _('Be the first to contribute a review for this application')
1484
 
 
1485
 
        EmbeddedMessage.__init__(self, title, msg, 'text-editor')
1486
 
        return
1487
 
 
1488
 
 
1489
 
 
1490
 
if __name__ == "__main__":
1491
 
    w = StarRatingSelector()
1492
 
    #~ w.set_avg_rating(3.5)
1493
 
    #~ w.set_nr_reviews(101)
1494
 
 
1495
 
    l = gtk.Label('focus steeler')
1496
 
    l.set_selectable(True)
1497
 
 
1498
 
    vb = gtk.VBox(spacing=6)
1499
 
    vb.pack_start(l)
1500
 
    vb.pack_start(w)
1501
 
 
1502
 
    win = gtk.Window()
1503
 
    win.add(vb)
1504
 
    win.show_all()
1505
 
    win.connect('destroy', gtk.main_quit)
1506
 
 
1507
 
    gtk.main()