CoverArt Browser
v2.0
Browse your cover-art albums in Rhythmbox
|
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