2
# -*- coding: utf-8 -*-
4
# Copyright (C) 2010 Canonical
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.
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
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
34
from gettext import gettext as _
36
from mkit import EM, ShapeStar, get_mkit_theme
37
from softwarecenter.drawing import alpha_composite, color_floats
39
from softwarecenter.utils import (
41
get_person_from_config,
43
upstream_version_compare,
47
from softwarecenter.netstatus import network_state_is_connected
48
from softwarecenter.enums import PkgStates
49
from softwarecenter.backend.reviews import UsefulnessCache
51
LOG_ALLOCATION = logging.getLogger("softwarecenter.ui.gtk.allocation")
54
class StarPainter(object):
69
self.shape = ShapeStar(5, 0.575)
70
self.theme = get_mkit_theme()
72
self.fill = self.FILL_EMPTY
73
self.border = self.BORDER_OFF
74
self.paint_style = self.STYLE_FLAT
78
self.bg_color = color_floats('#989898') # gray
79
self.fg_color = color_floats('#FFa000') # yellow
82
def _paint_star(self, cr, widget, state, x, y, w, h, alpha=1.0):
85
if self.border == self.BORDER_ON:
86
self._paint_star_border(cr, widget, state, x, y, w, h, alpha)
88
if self.paint_style == self.STYLE_INTERACTIVE:
89
self._paint_star_interactive(cr, widget, state, x, y, w, h, alpha)
91
elif self.paint_style == self.STYLE_BIG:
92
self._paint_star_big(cr, widget, state, x, y, w, h, alpha)
94
elif self.paint_style == self.STYLE_FLAT:
95
self._paint_star_flat(cr, widget, state, x, y, w, h, alpha)
98
raise AttributeError, 'paint_style value not recognised!'
103
def _paint_half_star(self, cr, widget, state, x, y, w, h):
107
if widget.get_direction() != gtk.TEXT_DIR_RTL:
115
cr.rectangle(x0, y, w/2, h)
118
self.set_fill(self.FILL_FULL)
119
self._paint_star(cr, widget, state, x, y, w, h)
121
cairo.Context.reset_clip(cr)
123
cr.rectangle(x1, y, w/2, h)
126
self.set_fill(self.FILL_EMPTY)
127
self._paint_star(cr, widget, state, x, y, w, h)
129
cairo.Context.reset_clip(cr)
131
self.set_fill(self.FILL_HALF)
135
def _paint_star_border(self, cr, widget, state, x, y, w, h, alpha):
137
cr.set_line_join(cairo.LINE_CAP_ROUND)
139
self.shape.layout(cr, x, y, w, h)
140
sel_color = color_floats(widget.style.base[gtk.STATE_SELECTED])
142
cr.set_source_rgba(*sel_color+(0.65,))
148
def _paint_star_flat(self, cr, widget, state, x, y, w, h, alpha):
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)
155
if self.fill == self.FILL_EMPTY:
156
cr.set_source_rgb(*color_floats(widget.style.mid[gtk.STATE_NORMAL]))
159
cr.set_source_rgba(*self.fg_color)
161
self.shape.layout(cr, x, y, w, h)
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)
167
cr.set_source_color(widget.style.white)
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)
175
self._paint_star_interactive_out(cr, widget, state, *args)
178
def _paint_star_interactive_out(self, cr, widget, state, x, y, w, h, alpha):
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
186
if state == gtk.STATE_PRELIGHT:
187
fill = alpha_composite(self.fg_color+(0.75,), (1,1,1))
195
grad0, grad1 = theme.gradients[state]
196
grad0 = grad0.floats()
197
grad1 = grad1.floats()
199
white = theme.light_line[state].floats()
200
dark = theme.dark_line[state].floats()
203
cr.set_line_join(cairo.LINE_CAP_ROUND)
207
shape.layout(cr, x, y+1, w, h)
209
light = self.theme.light_line[state].floats()
210
cr.set_source_rgba(*light + (0.9,))
213
shape.layout(cr, x+1, y+1, w-2, h-2)
215
cr.set_source_rgb(*dark)
220
shape.layout(cr, x+1, y+1, w-2, h-2)
221
cr.set_source_rgb(*fill)
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,))
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,))
242
def _paint_star_interactive_in(self, cr, widget, state, x, y, w, h, alpha):
245
# define the color palate
246
#~ black = color_floats(widget.style.black)
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)
255
dark = darker = theme.dark_line[state].floats()
256
grad0, grad1 = theme.gradients[state]
257
grad0 = grad0.floats()
258
grad1 = grad1.floats()
260
cr.set_line_join(cairo.LINE_CAP_ROUND)
262
shape.layout(cr, x+1, y+1, w-2, h-2)
263
cr.set_source_rgba(*dark+(0.35,))
268
cr.set_source_rgba(*dark+(0.8,))
272
self.shape.layout(cr, x+1, y+1, w-2, h-2)
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)
282
self.shape.layout(cr, x+1, y+1, w-2, h-2)
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,))
289
#~ cr.set_source_rgba(*darker+(0.1*alpha,))
293
def _paint_star_big(self, cr, widget, state, x, y, w, h, alpha):
294
if self.fill == self.FILL_FULL:
296
dark = color_floats('#B54D00') # brownish
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])
305
cr.set_line_join(cairo.LINE_CAP_ROUND)
306
self.shape.layout(cr, x+2, y+2, w-4, h-4)
308
cr.set_source_rgba(*white+(0.5,))
312
cr.set_source_rgb(*dark)
316
cr.set_source_rgb(*fill)
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,))
327
def set_paint_style(self, paint_style):
328
self.paint_style = paint_style
331
def set_fill(self, fill):
335
def set_border(self, border_type):
336
self.border = border_type
339
def paint_star(self, cr, widget, state, x, y, w, h):
341
if self.fill == self.FILL_HALF:
342
self._paint_half_star(cr, widget, state, x, y, w, h)
345
self._paint_star(cr, widget, state, x, y, w, h, self.alpha)
348
def paint_rating(self, cr, widget, state, x, y, star_size, max_stars, rating):
351
direction = widget.get_direction()
353
index = range(0, max_stars)
354
if direction == gtk.TEXT_DIR_RTL: index.reverse()
359
self.set_fill(StarPainter.FILL_FULL)
361
elif (i == int(rating) and
362
rating - int(rating) > 0):
363
self.set_fill(StarPainter.FILL_HALF)
366
self.set_fill(StarPainter.FILL_EMPTY)
368
self.paint_star(cr, widget, state, x, y, *star_size)
373
class StarWidget(gtk.EventBox, StarPainter):
375
def __init__(self, size, is_interactive):
376
gtk.EventBox.__init__(self)
377
StarPainter.__init__(self)
379
self.set_visible_window(False)
380
self.set_size_request(*size)
382
self.is_interactive = is_interactive
384
self._init_event_handling()
386
self.connect('expose-event', self._on_expose)
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)
399
def _on_expose(self, widget, event):
400
cr = widget.window.cairo_create()
401
self.draw(cr, self.allocation)
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)
413
class SimpleStarRating(gtk.HBox, StarPainter):
416
STAR_SIZE = (int(1.25*EM), int(1.25*EM))
418
def __init__(self, n_stars=0, spacing=0, star_size=STAR_SIZE):
419
gtk.HBox.__init__(self, spacing=spacing)
420
StarPainter.__init__(self)
422
self.n_stars = n_stars
423
self.star_size = star_size
424
self.set_max_stars(self.MAX_STARS)
426
self.connect('expose-event', self._on_expose)
429
def _on_expose(self, widget, event):
430
cr = widget.window.cairo_create()
431
self.draw(cr, self.allocation)
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)
441
def draw(self, cr, a):
442
sw = self.get_property('width-request')/self.MAX_STARS
443
self.paint_rating(cr, self,
451
def queue_draw(self):
453
self.queue_draw_area(a.x-5, a.y-5, a.width+10, a.height+10)
456
def set_max_stars(self, max_stars):
457
self.max_stars = max_stars
458
self._calc_size(max_stars)
461
def set_rating(self, rating):
462
if rating is None: rating = 0
463
self.n_stars = rating
468
class StarRating(gtk.Alignment):
472
__gsignals__ = {'changed':(gobject.SIGNAL_RUN_LAST,
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)
481
self.set_padding(2, 2, 0, 0)
482
self.hbox = gtk.HBox(spacing=spacing)
487
self._build(star_size, is_interactive)
489
self.set_rating(n_stars)
491
def _build(self, star_size, is_interactive):
492
for i in range(self.MAX_STARS):
493
star = StarWidget(star_size, is_interactive)
495
self.hbox.pack_start(star, expand=False)
498
def set_paint_style(self, paint_style):
499
for star in self.get_stars():
500
star.set_paint_style(paint_style)
503
def set_rating(self, n_stars):
504
if n_stars is None: n_stars = 0
505
self.rating = n_stars
507
acc = self.get_accessible()
508
acc.set_name(_("%s star rating") % n_stars)
509
acc.set_description(_("%s star rating") % n_stars)
511
for i, star in enumerate(self.get_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)
517
star.set_fill(StarPainter.FILL_EMPTY)
523
def get_rating(self):
527
return filter(lambda x: isinstance(x, StarWidget), self.hbox.get_children())
530
class StarRatingSelector(StarRating):
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
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)
543
for star in self.get_stars():
544
self._connect_signals(star)
546
self.set_paint_style(StarPainter.STYLE_INTERACTIVE)
551
def _on_enter(self, star, event):
552
star.set_state(gtk.STATE_PRELIGHT)
553
self.set_tentative_rating(star.position+1)
555
self.caption.set_markup(_(self.RATING_WORDS[star.position+1]))
558
def _on_leave(self, star, event):
559
star.set_state(gtk.STATE_NORMAL)
560
gobject.timeout_add(100, self._hover_check_cb)
564
def _on_press(self, star, event):
565
a_star_has_focus = filter(lambda s: s.has_focus(),
567
if a_star_has_focus: star.grab_focus()
569
star.set_state(gtk.STATE_ACTIVE)
573
def _on_release(self, star, event):
574
self.set_rating(star.position+1)
575
star.set_state(gtk.STATE_PRELIGHT)
579
def _on_focus_in(self, star, event):
580
self.set_tentative_rating(star.position+1)
583
def _on_focus_out(self, star, event):
584
self.set_tentative_rating(0)
587
def _on_key_press(self, star, event):
589
if kv == gtk.keysyms.space or kv == gtk.keysyms.Return:
590
star.set_state(gtk.STATE_ACTIVE)
592
star.queue_draw_area(a.x-2, a.y-2, a.width+4, a.height+4)
593
elif kv == gtk.keysyms._1:
595
elif kv == gtk.keysyms._2:
597
elif kv == gtk.keysyms._3:
599
elif kv == gtk.keysyms._4:
601
elif kv == gtk.keysyms._5:
604
self.caption.set_markup(_(self.RATING_WORDS[self.rating]))
607
def _on_key_release(self, star, event):
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)
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)
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)
631
self.caption.set_markup(_(self.RATING_WORDS[self.rating]))
634
def queue_draw(self):
636
self.queue_draw_area(a.x-5, a.y-5, a.width+10, a.height+10)
639
def set_caption_widget(self, caption_widget):
640
caption_widget.set_markup(_(self.RATING_WORDS[0]))
641
self.caption = caption_widget
644
def set_tentative_rating(self, n_stars):
645
for i, star in enumerate(self.get_stars()):
647
star.border = StarPainter.BORDER_ON
649
star.border = StarPainter.BORDER_OFF
654
class StarCaption(gtk.Label):
657
gtk.Label.__init__(self)
658
#self.shape = ShapeRoundedRectangle()
659
#self.connect('expose-event', self._on_expose)
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)
672
def set_markup(self, markup):
673
gtk.Label.set_markup(self, '<small>%s</small>' % markup)
677
class ReviewStatsContainer(gtk.VBox):
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)
689
def set_avg_rating(self, avg_rating):
690
self.star_rating.set_rating(avg_rating)
692
def set_nr_reviews(self, nr_reviews):
693
self.nr_reviews = nr_reviews
694
self._update_nr_reviews()
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)
705
class UIReviewsList(gtk.VBox):
708
'new-review':(gobject.SIGNAL_RUN_FIRST,
711
'report-abuse':(gobject.SIGNAL_RUN_FIRST,
713
(gobject.TYPE_PYOBJECT,)),
714
'submit-usefulness':(gobject.SIGNAL_RUN_FIRST,
716
(gobject.TYPE_PYOBJECT, bool)),
717
'modify-review':(gobject.SIGNAL_RUN_FIRST,
719
(gobject.TYPE_PYOBJECT,)),
720
'delete-review':(gobject.SIGNAL_RUN_FIRST,
722
(gobject.TYPE_PYOBJECT,)),
723
'more-reviews-clicked':(gobject.SIGNAL_RUN_FIRST,
726
'different-review-language-clicked':(gobject.SIGNAL_RUN_FIRST,
728
(gobject.TYPE_STRING,) ),
731
def __init__(self, parent):
732
gtk.VBox.__init__(self)
733
self.logged_in_person = get_person_from_config()
735
self._parent = parent
736
# this is a list of review data (softwarecenter.backend.reviews.Review)
738
# global review stats, this includes ratings in different languages
739
self.global_review_stats = None
741
self.useful_votes = UsefulnessCache()
742
self.logged_in_person = None
744
label = mkit.EtchedLabel(_("Reviews"))
745
label.set_padding(6, 6)
746
label.set_use_markup(True)
747
label.set_alignment(0, 0.5)
749
self.new_review = mkit.VLinkButton(_("Write your own review"))
750
self.new_review.set_underline(True)
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)
757
self.vbox = gtk.VBox(spacing=24)
758
self.vbox.set_border_width(6)
759
self.pack_start(self.vbox)
761
self.new_review.connect('clicked', lambda w: self.emit('new-review'))
765
def _on_button_new_clicked(self, button):
766
self.emit("new-review")
768
def update_useful_votes(self, my_votes):
769
self.useful_votes = my_votes
772
""" take the review data object from self.reviews and build the
775
self.logged_in_person = get_person_from_config()
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)
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())
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()
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:
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)
812
def _clear_vbox(self, vbox):
813
children = vbox.get_children()
814
for child in children:
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
823
#print 'Review count: %s' % len(self.reviews)
826
self.install_first_label.hide()
827
except AttributeError:
830
self._clear_vbox(self.vbox)
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)
837
self._add_no_network_connection_msg()
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)
843
self.new_review.show()
845
self.new_review.hide()
846
self._install_to_review()
848
# always hide spinner and call _fill (fine if there is nothing to do)
854
# adjust label if we have reviews
855
if self._any_reviews_current_user():
856
self.new_review.hide()
858
self.new_review.set_label(_("Write your own review"))
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()
864
self.vbox.pack_start(NoReviewYet())
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
872
button = gtk.Button(_("Show reviews in english"))
874
"clicked", self._on_different_review_language_clicked)
876
self.vbox.pack_start(button)
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:
884
button = gtk.Button(_("Check for more reviews"))
885
button.connect("clicked", self._on_more_reviews_clicked)
887
self.vbox.pack_start(button)
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")
895
def _on_different_review_language_clicked(self, button):
897
self.vbox.remove(button)
898
self.emit("different-review-language-clicked", language)
900
def add_review(self, review):
901
self.reviews.append(review)
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)
913
def remove_review(self, review):
914
for r in self.reviews:
915
if r.id == review.id:
916
self.reviews.remove(r)
920
def get_all_review_ids(self):
922
for review in self.reviews:
923
ids.append(review.id)
928
for review in self.vbox:
930
self.new_review.hide()
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):
936
self.install_first_label.hide()
937
except AttributeError:
940
a = gtk.Alignment(0.5, 0.5)
942
hb = gtk.HBox(spacing=12)
945
spinner = gtk.Spinner()
948
hb.pack_start(spinner, False)
950
l = mkit.EtchedLabel(message)
951
l.set_use_markup(True)
953
hb.pack_start(l, False)
955
self.vbox.pack_start(a, False)
958
def hide_spinner(self):
959
for child in self.vbox.get_children():
960
if isinstance(child, gtk.Alignment):
964
def draw(self, cr, a):
966
if isinstance(r, (UIReview)):
967
r.draw(cr, r.allocation)
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())
974
class UIReview(gtk.VBox):
975
""" the UI for a individual review including all button to mark
976
useful/inappropriate etc
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)
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()
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
1010
self.pack_start(self.header, False)
1011
self.pack_start(self.body, False)
1012
self.pack_start(self.footer_split, False)
1014
self.logged_in_person = logged_in_person
1017
self.useful_votes = useful_votes
1019
self._allocation = None
1022
self.connect('realize',
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)
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!")
1038
self._allocation = allocation
1040
summary.set_size_request(max(20, allocation.width - \
1041
stars.allocation.width - \
1042
who_when.allocation.width - 20), -1)
1044
text.set_size_request(allocation.width, -1)
1047
version_lbl.set_size_request(allocation.width-flag.allocation.width-20, -1)
1050
def _on_report_abuse_clicked(self, button):
1051
reviews = self.get_ancestor(UIReviewsList)
1053
reviews.emit("report-abuse", self.id)
1055
def _on_modify_clicked(self, button):
1056
reviews = self.get_ancestor(UIReviewsList)
1058
reviews.emit("modify-review", self.id)
1060
def _on_useful_clicked(self, btn, is_useful):
1061
reviews = self.get_ancestor(UIReviewsList)
1063
self._usefulness_ui_update('progress')
1064
reviews.emit("submit-usefulness", self.id, is_useful)
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)
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
1074
self._build_usefulness_ui(current_user_reviewer, useful_total, useful_favorable, self.useful_votes)
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()
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)
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"
1106
widget = getattr(self, attr, None)
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')
1115
def _delete_ui_update(self, type, current_user_reviewer=False, action=None):
1116
self._hide_delete_elements()
1118
self._build_delete_flag_ui(current_user_reviewer)
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()
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)
1144
def _on_delete_clicked(self, btn):
1145
reviews = self.get_ancestor(UIReviewsList)
1147
self._delete_ui_update('progress')
1148
reviews.emit("delete-review", self.id)
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)
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"
1160
o = getattr(self, attr, None)
1165
def _build(self, review_data, app_version, logged_in_person, useful_votes):
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)
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
1183
dark_color = self.style.dark[gtk.STATE_NORMAL]
1184
m = self._whom_when_markup(self.person, displayname, cur_t, dark_color)
1186
who_when = mkit.EtchedLabel(m)
1187
who_when.set_use_markup(True)
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)
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)
1200
stars = SimpleStarRating(review_data.rating)
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)
1207
#if review version is different to version of app being displayed,
1210
if (review_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))
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)
1226
self.footer_split.pack_start(self.footer, False)
1228
current_user_reviewer = False
1229
if self.person == self.logged_in_person:
1230
current_user_reviewer = True
1232
self._build_usefulness_ui(current_user_reviewer, useful_total,
1233
useful_favorable, useful_votes, useful_submit_error)
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)
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)
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)
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)
1271
self.yes_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()
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
1288
if network_state_is_connected():
1293
# showing "was this useful is not interessting"
1294
if self.useful_total == 0:
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
1302
if already_voted == None:
1303
if useful_total == 0 and current_user_reviewer:
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,
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,
1329
#only display these special strings if the user voted either way
1331
if useful_total == 1:
1332
s = _("You found this review helpful.")
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,
1343
if useful_total == 1:
1344
s = _("You found this review unhelpful.")
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,
1356
return gtk.Label('<small>%s</small>' % s)
1358
def _build_delete_flag_ui(self, current_user_reviewer, delete_error=False, modify_error=False):
1360
self._delete_ui_update('error', current_user_reviewer, 'deleting')
1362
self._delete_ui_update('error', current_user_reviewer, 'modifying')
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)
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()
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
1392
# prefer displayname if available
1393
correct_name = displayname or person
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"
1402
gobject.markup_escape_text(nice_date))
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))
1411
def draw(self, cr, a):
1413
if not self.person == self.logged_in_person:
1418
color = mkit.floats_from_gdkcolor(self.style.mid[self.state])
1419
cr.set_source_rgba(*color+(0.2,))
1425
class EmbeddedMessage(UIReview):
1427
def __init__(self, title='', message='', icon_name=''):
1428
UIReview.__init__(self)
1432
a = gtk.Alignment(0.5, 0.5)
1433
self.body.pack_start(a, False)
1435
hb = gtk.HBox(spacing=12)
1439
self.image = gtk.image_new_from_icon_name(icon_name,
1440
gtk.ICON_SIZE_DIALOG)
1441
hb.pack_start(self.image, False)
1443
self.label = gtk.Label()
1444
self.label.set_line_wrap(True)
1445
self.label.set_alignment(0, 0.5)
1448
self.label.set_markup('<b><big>%s</big></b>\n%s' % (title, message))
1450
self.label.set_markup(message)
1452
hb.pack_start(self.label)
1457
def draw(self, cr, a):
1460
color = mkit.floats_from_gdkcolor(self.style.mid[self.state])
1461
cr.set_source_rgba(*color+(0.2,))
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)
1476
class NoReviewYetWriteOne(EmbeddedMessage):
1477
""" represents if there are no reviews yet and the app is installed """
1478
def __init__(self, *args, **kwargs):
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')
1485
EmbeddedMessage.__init__(self, title, msg, 'text-editor')
1490
if __name__ == "__main__":
1491
w = StarRatingSelector()
1492
#~ w.set_avg_rating(3.5)
1493
#~ w.set_nr_reviews(101)
1495
l = gtk.Label('focus steeler')
1496
l.set_selectable(True)
1498
vb = gtk.VBox(spacing=6)
1505
win.connect('destroy', gtk.main_quit)