CoverArt Browser  v2.0
Browse your cover-art albums in Rhythmbox
/home/foss/Downloads/coverart-browser/stars.py
00001 # Copyright (C) 2011 Canonical
00002 #
00003 # Authors:
00004 #  Matthew McGowan
00005 #
00006 # This program is free software; you can redistribute it and/or modify it under
00007 # the terms of the GNU General Public License as published by the Free Software
00008 # Foundation; version 3.
00009 #
00010 # This program is distributed in the hope that it will be useful, but WITHOUT
00011 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
00012 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
00013 # details.
00014 #
00015 # You should have received a copy of the GNU General Public License along with
00016 # this program; if not, write to the Free Software Foundation, Inc.,
00017 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
00018 
00019 import logging
00020 import gettext
00021 
00022 import cairo
00023 from gi.repository import Gtk, Gdk, GObject
00024 
00025 from em import StockEms, em, small_em, big_em
00026 
00027 
00028 _star_surface_cache = {}
00029 
00030 LOG = logging.getLogger(__name__)
00031 
00032 
00033 class StarSize:
00034     SMALL = 1
00035     NORMAL = 2
00036     BIG = 3
00037     PIXEL_VALUE = 4
00038 
00039 
00040 class StarFillState:
00041     FULL = 10
00042     EMPTY = 20
00043 
00044 
00045 class StarRenderHints:
00046     NORMAL = 1
00047     REACTIVE = -1
00048 
00049 
00050 class ShapeStar():
00051     def __init__(self, points, indent=0.61):
00052         self.coords = self._calc_coords(points, 1 - indent)
00053 
00054     def _calc_coords(self, points, indent):
00055         coords = []
00056 
00057         from math import cos, pi, sin
00058 
00059         step = pi / points
00060 
00061         for i in range(2 * points):
00062             if i % 2:
00063                 x = (sin(step * i) + 1) * 0.5
00064                 y = (cos(step * i) + 1) * 0.5
00065             else:
00066                 x = (sin(step * i) * indent + 1) * 0.5
00067                 y = (cos(step * i) * indent + 1) * 0.5
00068 
00069             coords.append((x, y))
00070         return coords
00071 
00072     def layout(self, cr, x, y, w, h):
00073         points = [(sx_sy[0] * w + x, sx_sy[1] * h + y)
00074                   for sx_sy in self.coords]
00075         cr.move_to(*points[0])
00076 
00077         for p in points[1:]:
00078             cr.line_to(*p)
00079 
00080         cr.close_path()
00081 
00082 
00083 class StarRenderer(ShapeStar):
00084     def __init__(self):
00085         ShapeStar.__init__(self, 5, 0.6)
00086 
00087         self.size = StarSize.NORMAL
00088         self.n_stars = 5
00089         self.spacing = 1
00090         self.rounded = True
00091         self.rating = 3
00092         self.hints = StarRenderHints.NORMAL
00093 
00094         self.pixel_value = None
00095         self._size_map = {StarSize.SMALL: small_em,
00096                           StarSize.NORMAL: em,
00097                           StarSize.BIG: big_em,
00098                           StarSize.PIXEL_VALUE: self.get_pixel_size}
00099 
00100     # private
00101     def _get_mangled_keys(self, size):
00102         keys = (size * self.hints + StarFillState.FULL,
00103                 size * self.hints + StarFillState.EMPTY)
00104         return keys
00105 
00106     # public
00107     def create_normal_surfaces(self,
00108                                context, vis_width, vis_height, star_width):
00109 
00110         rgba1 = context.get_border_color(Gtk.StateFlags.NORMAL)
00111         rgba0 = context.get_color(Gtk.StateFlags.ACTIVE)
00112 
00113         lin = cairo.LinearGradient(0, 0, 0, vis_height)
00114         lin.add_color_stop_rgb(0, rgba0.red, rgba0.green, rgba0.blue)
00115         lin.add_color_stop_rgb(1, rgba1.red, rgba1.green, rgba1.blue)
00116 
00117         # paint full
00118         full_surf = cairo.ImageSurface(
00119             cairo.FORMAT_ARGB32, vis_width, vis_height)
00120 
00121         cr = cairo.Context(full_surf)
00122         cr.set_source(lin)
00123         cr.set_line_width(1)
00124         if self.rounded:
00125             cr.set_line_join(cairo.LINE_CAP_ROUND)
00126 
00127         for i in range(self.n_stars):
00128             x = 1 + i * (star_width + self.spacing)
00129             self.layout(cr, x + 1, 1, star_width - 2, vis_height - 2)
00130             cr.stroke_preserve()
00131             cr.fill()
00132 
00133         del cr
00134 
00135         # paint empty
00136         empty_surf = cairo.ImageSurface(
00137             cairo.FORMAT_ARGB32, vis_width, vis_height)
00138 
00139         cr = cairo.Context(empty_surf)
00140         cr.set_source(lin)
00141         cr.set_line_width(1)
00142         if self.rounded:
00143             cr.set_line_join(cairo.LINE_CAP_ROUND)
00144 
00145         for i in range(self.n_stars):
00146             x = 1 + i * (star_width + self.spacing)
00147             self.layout(cr, x + 1, 1, star_width - 2, vis_height - 2)
00148             cr.stroke()
00149 
00150         del cr
00151 
00152         return full_surf, empty_surf
00153 
00154     def create_reactive_surfaces(self,
00155                                  context, vis_width, vis_height, star_width):
00156 
00157         # paint full
00158         full_surf = cairo.ImageSurface(
00159             cairo.FORMAT_ARGB32, vis_width, vis_height)
00160 
00161         cr = cairo.Context(full_surf)
00162         if self.rounded:
00163             cr.set_line_join(cairo.LINE_CAP_ROUND)
00164 
00165         for i in range(self.n_stars):
00166             x = 1 + i * (star_width + self.spacing)
00167             self.layout(cr, x + 2, 2, star_width - 4, vis_height - 4)
00168 
00169         line_color = context.get_border_color(Gtk.StateFlags.NORMAL)
00170         cr.set_source_rgb(line_color.red, line_color.green,
00171                           line_color.blue)
00172 
00173         cr.set_line_width(3)
00174         cr.stroke_preserve()
00175         cr.clip()
00176 
00177         context.save()
00178         context.add_class("button")
00179         context.set_state(Gtk.StateFlags.NORMAL)
00180 
00181         Gtk.render_background(context, cr, 0, 0, vis_width, vis_height)
00182 
00183         context.restore()
00184 
00185         for i in range(self.n_stars):
00186             x = 1 + i * (star_width + self.spacing)
00187             self.layout(cr, x + 1.5, 1.5, star_width - 3, vis_height - 3)
00188 
00189         cr.set_source_rgba(1, 1, 1, 0.8)
00190         cr.set_line_width(1)
00191         cr.stroke()
00192 
00193         del cr
00194 
00195         # paint empty
00196         empty_surf = cairo.ImageSurface(
00197             cairo.FORMAT_ARGB32, vis_width, vis_height)
00198 
00199         cr = cairo.Context(empty_surf)
00200         if self.rounded:
00201             cr.set_line_join(cairo.LINE_CAP_ROUND)
00202 
00203         line_color = context.get_border_color(Gtk.StateFlags.NORMAL)
00204         cr.set_source_rgb(line_color.red, line_color.green,
00205                           line_color.blue)
00206 
00207         for i in range(self.n_stars):
00208             x = 1 + i * (star_width + self.spacing)
00209             self.layout(cr, x + 2, 2, star_width - 4, vis_height - 4)
00210 
00211         cr.set_line_width(3)
00212         cr.stroke()
00213 
00214         del cr
00215 
00216         return full_surf, empty_surf
00217 
00218     def update_cache_surfaces(self, context, size):
00219         LOG.debug('update cache')
00220         global _star_surface_cache
00221 
00222         star_width = vis_height = self._size_map[size]()
00223         vis_width = (star_width + self.spacing) * self.n_stars
00224 
00225         if self.hints == StarRenderHints.NORMAL:
00226             surfs = self.create_normal_surfaces(context, vis_width,
00227                                                 vis_height, star_width)
00228 
00229         elif self.hints == StarRenderHints.REACTIVE:
00230             surfs = self.create_reactive_surfaces(
00231                 context, vis_width,
00232                 vis_height, star_width)
00233 
00234         # dict keys
00235         full_key, empty_key = self._get_mangled_keys(size)
00236         # save surfs to dict
00237         _star_surface_cache[full_key] = surfs[0]
00238         _star_surface_cache[empty_key] = surfs[1]
00239         return surfs
00240 
00241     def lookup_surfaces_for_size(self, size):
00242         full_key, empty_key = self._get_mangled_keys(size)
00243 
00244         if full_key not in _star_surface_cache:
00245             return None, None
00246 
00247         full_surf = _star_surface_cache[full_key]
00248         empty_surf = _star_surface_cache[empty_key]
00249         return full_surf, empty_surf
00250 
00251     def render_star(self, context, cr, x, y):
00252         size = self.size
00253 
00254         full, empty = self.lookup_surfaces_for_size(size)
00255         if full is None:
00256             full, empty = self.update_cache_surfaces(context, size)
00257 
00258         fraction = self.rating / self.n_stars
00259 
00260         stars_width = star_height = full.get_width()
00261 
00262         full_width = round(fraction * stars_width, 0)
00263         cr.rectangle(x, y, full_width, star_height)
00264         cr.clip()
00265         cr.set_source_surface(full, x, y)
00266         cr.paint()
00267         cr.reset_clip()
00268 
00269         if fraction < 1.0:
00270             empty_width = stars_width - full_width
00271             cr.rectangle(x + full_width, y, empty_width, star_height)
00272             cr.clip()
00273             cr.set_source_surface(empty, x, y)
00274             cr.paint()
00275             cr.reset_clip()
00276 
00277     def get_pixel_size(self):
00278         return self.pixel_value
00279 
00280     def get_visible_size(self, context):
00281         surf, _ = self.lookup_surfaces_for_size(self.size)
00282         if surf is None:
00283             surf, _ = self.update_cache_surfaces(context, self.size)
00284         return surf.get_width(), surf.get_height()
00285 
00286 
00287 class Star(Gtk.EventBox, StarRenderer):
00288     def __init__(self, size=StarSize.NORMAL):
00289         Gtk.EventBox.__init__(self)
00290         StarRenderer.__init__(self)
00291         self.set_name("featured-star")
00292 
00293         self.label = None
00294         self.size = size
00295 
00296         self.xalign = 0.5
00297         self.yalign = 0.5
00298 
00299         self._render_allocation_bbox = False
00300 
00301         self.set_visible_window(False)
00302         self.connect("draw", self.on_draw)
00303         self.connect("style-updated", self.on_style_updated)
00304 
00305     def do_get_preferred_width(self):
00306         context = self.get_style_context()
00307         pref_w, _ = self.get_visible_size(context)
00308         return pref_w, pref_w
00309 
00310     def do_get_preferred_height(self):
00311         context = self.get_style_context()
00312         _, pref_h = self.get_visible_size(context)
00313         return pref_h, pref_h
00314 
00315     def set_alignment(self, xalign, yalign):
00316         self.xalign = xalign
00317         self.yalign = yalign
00318         self.queue_draw()
00319 
00320         #~ def set_padding(*args):
00321         #~ return
00322 
00323     def get_alignment(self):
00324         return self.xalign, self.yalign
00325 
00326         #~ def get_padding(*args):
00327         #~ return
00328 
00329     def on_style_updated(self, widget):
00330         global _star_surface_cache
00331         _star_surface_cache = {}
00332         self.queue_draw()
00333 
00334     def on_draw(self, widget, cr):
00335         self.render_star(widget.get_style_context(), cr, 0, 0)
00336 
00337         if self._render_allocation_bbox:
00338             a = widget.get_allocation()
00339             cr.rectangle(0, 0, a.width, a.height)
00340             cr.set_source_rgb(1, 0, 0)
00341             cr.set_line_width(2)
00342             cr.stroke()
00343 
00344     def set_n_stars(self, n_stars):
00345         if n_stars == self.n_stars:
00346             return
00347 
00348         self.n_stars = n_stars
00349         global _star_surface_cache
00350         _star_surface_cache = {}
00351         self.queue_draw()
00352 
00353     def set_rating(self, rating):
00354         self.rating = float(rating)
00355         self.queue_draw()
00356 
00357     def set_avg_rating(self, rating):
00358         # compat for ratings container
00359         return self.set_rating(rating)
00360 
00361     def set_size(self, size):
00362         self.size = size
00363         self.queue_draw()
00364 
00365     def set_size_big(self):
00366         return self.set_size(StarSize.BIG)
00367 
00368     def set_size_small(self):
00369         return self.set_size(StarSize.SMALL)
00370 
00371     def set_size_normal(self):
00372         return self.set_size(StarSize.NORMAL)
00373 
00374     def set_use_rounded_caps(self, use_rounded):
00375         self.rounded = use_rounded
00376         global _star_surface_cache
00377         _star_surface_cache = {}
00378         self.queue_draw()
00379 
00380     def set_size_as_pixel_value(self, pixel_value):
00381         if pixel_value == self.pixel_value:
00382             return
00383 
00384         global _star_surface_cache
00385         keys = (StarSize.PIXEL_VALUE + StarFillState.FULL,
00386                 StarSize.PIXEL_VALUE + StarFillState.EMPTY)
00387 
00388         for key in keys:
00389             if key in _star_surface_cache:
00390                 del _star_surface_cache[key]
00391 
00392         self.pixel_value = pixel_value
00393         self.set_size(StarSize.PIXEL_VALUE)
00394 
00395 
00396 class StarRatingsWidget(Gtk.HBox):
00397     def __init__(self):
00398         Gtk.Box.__init__(self)
00399         self.set_spacing(StockEms.SMALL)
00400         self.stars = Star()
00401         self.stars.set_size_small()
00402         self.pack_start(self.stars, False, False, 0)
00403         self.label = Gtk.Label()
00404         self.label.set_alignment(0, 0.5)
00405         self.pack_start(self.label, False, False, 0)
00406 
00407     def set_avg_rating(self, rating):
00408         # compat for ratings container
00409         return self.stars.set_rating(rating)
00410 
00411     def set_nr_reviews(self, nr_reviews):
00412         s = gettext.ngettext(
00413             "%(nr_ratings)i rating",
00414             "%(nr_ratings)i ratings",
00415             nr_reviews) % {'nr_ratings': nr_reviews}
00416 
00417         # FIXME don't use fixed color
00418         m = '<span color="#8C8C8C"><small>(%s)</small></span>'
00419         self.label.set_markup(m % s)
00420 
00421 
00422 class ReactiveStar(Star):
00423     __gsignals__ = {
00424         "changed": (GObject.SignalFlags.RUN_LAST,
00425                     None,
00426                     (),)
00427     }
00428 
00429     def __init__(self, size=StarSize.SMALL):
00430         Star.__init__(self, size)
00431         self.hints = StarRenderHints.NORMAL
00432         self.set_rating(0)
00433 
00434         self.set_can_focus(True)
00435         self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK |
00436                         Gdk.EventMask.BUTTON_RELEASE_MASK |
00437                         Gdk.EventMask.KEY_RELEASE_MASK |
00438                         Gdk.EventMask.KEY_PRESS_MASK |
00439                         Gdk.EventMask.ENTER_NOTIFY_MASK |
00440                         Gdk.EventMask.LEAVE_NOTIFY_MASK)
00441 
00442         self.connect("enter-notify-event", self.on_enter_notify)
00443         self.connect("leave-notify-event", self.on_leave_notify)
00444         self.connect("button-press-event", self.on_button_press)
00445         self.connect("button-release-event", self.on_button_release)
00446         self.connect("key-press-event", self.on_key_press)
00447         self.connect("key-release-event", self.on_key_release)
00448         self.connect("focus-in-event", self.on_focus_in)
00449         self.connect("focus-out-event", self.on_focus_out)
00450 
00451     # signal handlers
00452     def on_enter_notify(self, widget, event):
00453         pass
00454 
00455     def on_leave_notify(self, widget, event):
00456         pass
00457 
00458     def on_button_press(self, widget, event):
00459         pass
00460 
00461     def on_button_release(self, widget, event):
00462         star_index = self.get_star_at_xy(event.x, event.y)
00463         if star_index is None:
00464             return
00465 
00466         if self.get_rating() == 1 and star_index == 1:
00467             star_index = 0
00468 
00469         self.set_rating(star_index)
00470         self.emit('changed')
00471 
00472     def on_key_press(self, widget, event):
00473         pass
00474 
00475     def on_key_release(self, widget, event):
00476         pass
00477 
00478     def on_focus_in(self, widget, event):
00479         pass
00480 
00481     def on_focus_out(self, widget, event):
00482         pass
00483 
00484     # public
00485     def get_rating(self):
00486         return self.rating
00487 
00488     def render_star(self, widget, cr, x, y):
00489         # paint focus
00490 
00491         StarRenderer.render_star(self, widget, cr, x, y)
00492         # if a star is hovered paint prelit star
00493 
00494     def get_star_at_xy(self, x, y, half_star_precision=False):
00495         star_width = self._size_map[self.size]()
00496 
00497         star_index = x / star_width
00498         remainder = 1.0
00499 
00500         if half_star_precision:
00501             if round((x % star_width) / star_width, 1) <= 0.5:
00502                 remainder = 0.5
00503 
00504         if star_index > self.n_stars:
00505             return None
00506 
00507         return int(star_index) + remainder
 All Classes Functions