Arquivos
coverart-browser/stars.py
T

508 linhas
14 KiB
Python

# Copyright (C) 2011 Canonical
#
# Authors:
# Matthew McGowan
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import logging
import gettext
import cairo
from gi.repository import Gtk, Gdk, GObject
from em import StockEms, em, small_em, big_em
_star_surface_cache = {}
LOG = logging.getLogger(__name__)
class StarSize:
SMALL = 1
NORMAL = 2
BIG = 3
PIXEL_VALUE = 4
class StarFillState:
FULL = 10
EMPTY = 20
class StarRenderHints:
NORMAL = 1
REACTIVE = -1
class ShapeStar():
def __init__(self, points, indent=0.61):
self.coords = self._calc_coords(points, 1 - indent)
def _calc_coords(self, points, indent):
coords = []
from math import cos, pi, sin
step = pi / points
for i in range(2 * points):
if i % 2:
x = (sin(step * i) + 1) * 0.5
y = (cos(step * i) + 1) * 0.5
else:
x = (sin(step * i) * indent + 1) * 0.5
y = (cos(step * i) * indent + 1) * 0.5
coords.append((x, y))
return coords
def layout(self, cr, x, y, w, h):
points = [(sx_sy[0] * w + x, sx_sy[1] * h + y)
for sx_sy in self.coords]
cr.move_to(*points[0])
for p in points[1:]:
cr.line_to(*p)
cr.close_path()
class StarRenderer(ShapeStar):
def __init__(self):
ShapeStar.__init__(self, 5, 0.6)
self.size = StarSize.NORMAL
self.n_stars = 5
self.spacing = 1
self.rounded = True
self.rating = 3
self.hints = StarRenderHints.NORMAL
self.pixel_value = None
self._size_map = {StarSize.SMALL: small_em,
StarSize.NORMAL: em,
StarSize.BIG: big_em,
StarSize.PIXEL_VALUE: self.get_pixel_size}
# private
def _get_mangled_keys(self, size):
keys = (size * self.hints + StarFillState.FULL,
size * self.hints + StarFillState.EMPTY)
return keys
# public
def create_normal_surfaces(self,
context, vis_width, vis_height, star_width):
rgba1 = context.get_border_color(Gtk.StateFlags.NORMAL)
rgba0 = context.get_color(Gtk.StateFlags.ACTIVE)
lin = cairo.LinearGradient(0, 0, 0, vis_height)
lin.add_color_stop_rgb(0, rgba0.red, rgba0.green, rgba0.blue)
lin.add_color_stop_rgb(1, rgba1.red, rgba1.green, rgba1.blue)
# paint full
full_surf = cairo.ImageSurface(
cairo.FORMAT_ARGB32, vis_width, vis_height)
cr = cairo.Context(full_surf)
cr.set_source(lin)
cr.set_line_width(1)
if self.rounded:
cr.set_line_join(cairo.LINE_CAP_ROUND)
for i in range(self.n_stars):
x = 1 + i * (star_width + self.spacing)
self.layout(cr, x + 1, 1, star_width - 2, vis_height - 2)
cr.stroke_preserve()
cr.fill()
del cr
# paint empty
empty_surf = cairo.ImageSurface(
cairo.FORMAT_ARGB32, vis_width, vis_height)
cr = cairo.Context(empty_surf)
cr.set_source(lin)
cr.set_line_width(1)
if self.rounded:
cr.set_line_join(cairo.LINE_CAP_ROUND)
for i in range(self.n_stars):
x = 1 + i * (star_width + self.spacing)
self.layout(cr, x + 1, 1, star_width - 2, vis_height - 2)
cr.stroke()
del cr
return full_surf, empty_surf
def create_reactive_surfaces(self,
context, vis_width, vis_height, star_width):
# paint full
full_surf = cairo.ImageSurface(
cairo.FORMAT_ARGB32, vis_width, vis_height)
cr = cairo.Context(full_surf)
if self.rounded:
cr.set_line_join(cairo.LINE_CAP_ROUND)
for i in range(self.n_stars):
x = 1 + i * (star_width + self.spacing)
self.layout(cr, x + 2, 2, star_width - 4, vis_height - 4)
line_color = context.get_border_color(Gtk.StateFlags.NORMAL)
cr.set_source_rgb(line_color.red, line_color.green,
line_color.blue)
cr.set_line_width(3)
cr.stroke_preserve()
cr.clip()
context.save()
context.add_class("button")
context.set_state(Gtk.StateFlags.NORMAL)
Gtk.render_background(context, cr, 0, 0, vis_width, vis_height)
context.restore()
for i in range(self.n_stars):
x = 1 + i * (star_width + self.spacing)
self.layout(cr, x + 1.5, 1.5, star_width - 3, vis_height - 3)
cr.set_source_rgba(1, 1, 1, 0.8)
cr.set_line_width(1)
cr.stroke()
del cr
# paint empty
empty_surf = cairo.ImageSurface(
cairo.FORMAT_ARGB32, vis_width, vis_height)
cr = cairo.Context(empty_surf)
if self.rounded:
cr.set_line_join(cairo.LINE_CAP_ROUND)
line_color = context.get_border_color(Gtk.StateFlags.NORMAL)
cr.set_source_rgb(line_color.red, line_color.green,
line_color.blue)
for i in range(self.n_stars):
x = 1 + i * (star_width + self.spacing)
self.layout(cr, x + 2, 2, star_width - 4, vis_height - 4)
cr.set_line_width(3)
cr.stroke()
del cr
return full_surf, empty_surf
def update_cache_surfaces(self, context, size):
LOG.debug('update cache')
global _star_surface_cache
star_width = vis_height = self._size_map[size]()
vis_width = (star_width + self.spacing) * self.n_stars
if self.hints == StarRenderHints.NORMAL:
surfs = self.create_normal_surfaces(context, vis_width,
vis_height, star_width)
elif self.hints == StarRenderHints.REACTIVE:
surfs = self.create_reactive_surfaces(
context, vis_width,
vis_height, star_width)
# dict keys
full_key, empty_key = self._get_mangled_keys(size)
# save surfs to dict
_star_surface_cache[full_key] = surfs[0]
_star_surface_cache[empty_key] = surfs[1]
return surfs
def lookup_surfaces_for_size(self, size):
full_key, empty_key = self._get_mangled_keys(size)
if full_key not in _star_surface_cache:
return None, None
full_surf = _star_surface_cache[full_key]
empty_surf = _star_surface_cache[empty_key]
return full_surf, empty_surf
def render_star(self, context, cr, x, y):
size = self.size
full, empty = self.lookup_surfaces_for_size(size)
if full is None:
full, empty = self.update_cache_surfaces(context, size)
fraction = self.rating / self.n_stars
stars_width = star_height = full.get_width()
full_width = round(fraction * stars_width, 0)
cr.rectangle(x, y, full_width, star_height)
cr.clip()
cr.set_source_surface(full, x, y)
cr.paint()
cr.reset_clip()
if fraction < 1.0:
empty_width = stars_width - full_width
cr.rectangle(x + full_width, y, empty_width, star_height)
cr.clip()
cr.set_source_surface(empty, x, y)
cr.paint()
cr.reset_clip()
def get_pixel_size(self):
return self.pixel_value
def get_visible_size(self, context):
surf, _ = self.lookup_surfaces_for_size(self.size)
if surf is None:
surf, _ = self.update_cache_surfaces(context, self.size)
return surf.get_width(), surf.get_height()
class Star(Gtk.EventBox, StarRenderer):
def __init__(self, size=StarSize.NORMAL):
Gtk.EventBox.__init__(self)
StarRenderer.__init__(self)
self.set_name("featured-star")
self.label = None
self.size = size
self.xalign = 0.5
self.yalign = 0.5
self._render_allocation_bbox = False
self.set_visible_window(False)
self.connect("draw", self.on_draw)
self.connect("style-updated", self.on_style_updated)
def do_get_preferred_width(self):
context = self.get_style_context()
pref_w, _ = self.get_visible_size(context)
return pref_w, pref_w
def do_get_preferred_height(self):
context = self.get_style_context()
_, pref_h = self.get_visible_size(context)
return pref_h, pref_h
def set_alignment(self, xalign, yalign):
self.xalign = xalign
self.yalign = yalign
self.queue_draw()
#~ def set_padding(*args):
#~ return
def get_alignment(self):
return self.xalign, self.yalign
#~ def get_padding(*args):
#~ return
def on_style_updated(self, widget):
global _star_surface_cache
_star_surface_cache = {}
self.queue_draw()
def on_draw(self, widget, cr):
self.render_star(widget.get_style_context(), cr, 0, 0)
if self._render_allocation_bbox:
a = widget.get_allocation()
cr.rectangle(0, 0, a.width, a.height)
cr.set_source_rgb(1, 0, 0)
cr.set_line_width(2)
cr.stroke()
def set_n_stars(self, n_stars):
if n_stars == self.n_stars:
return
self.n_stars = n_stars
global _star_surface_cache
_star_surface_cache = {}
self.queue_draw()
def set_rating(self, rating):
self.rating = float(rating)
self.queue_draw()
def set_avg_rating(self, rating):
# compat for ratings container
return self.set_rating(rating)
def set_size(self, size):
self.size = size
self.queue_draw()
def set_size_big(self):
return self.set_size(StarSize.BIG)
def set_size_small(self):
return self.set_size(StarSize.SMALL)
def set_size_normal(self):
return self.set_size(StarSize.NORMAL)
def set_use_rounded_caps(self, use_rounded):
self.rounded = use_rounded
global _star_surface_cache
_star_surface_cache = {}
self.queue_draw()
def set_size_as_pixel_value(self, pixel_value):
if pixel_value == self.pixel_value:
return
global _star_surface_cache
keys = (StarSize.PIXEL_VALUE + StarFillState.FULL,
StarSize.PIXEL_VALUE + StarFillState.EMPTY)
for key in keys:
if key in _star_surface_cache:
del _star_surface_cache[key]
self.pixel_value = pixel_value
self.set_size(StarSize.PIXEL_VALUE)
class StarRatingsWidget(Gtk.HBox):
def __init__(self):
Gtk.Box.__init__(self)
self.set_spacing(StockEms.SMALL)
self.stars = Star()
self.stars.set_size_small()
self.pack_start(self.stars, False, False, 0)
self.label = Gtk.Label()
self.label.set_alignment(0, 0.5)
self.pack_start(self.label, False, False, 0)
def set_avg_rating(self, rating):
# compat for ratings container
return self.stars.set_rating(rating)
def set_nr_reviews(self, nr_reviews):
s = gettext.ngettext(
"%(nr_ratings)i rating",
"%(nr_ratings)i ratings",
nr_reviews) % {'nr_ratings': nr_reviews}
# FIXME don't use fixed color
m = '<span color="#8C8C8C"><small>(%s)</small></span>'
self.label.set_markup(m % s)
class ReactiveStar(Star):
__gsignals__ = {
"changed": (GObject.SignalFlags.RUN_LAST,
None,
(),)
}
def __init__(self, size=StarSize.SMALL):
Star.__init__(self, size)
self.hints = StarRenderHints.NORMAL
self.set_rating(0)
self.set_can_focus(True)
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK |
Gdk.EventMask.KEY_RELEASE_MASK |
Gdk.EventMask.KEY_PRESS_MASK |
Gdk.EventMask.ENTER_NOTIFY_MASK |
Gdk.EventMask.LEAVE_NOTIFY_MASK)
self.connect("enter-notify-event", self.on_enter_notify)
self.connect("leave-notify-event", self.on_leave_notify)
self.connect("button-press-event", self.on_button_press)
self.connect("button-release-event", self.on_button_release)
self.connect("key-press-event", self.on_key_press)
self.connect("key-release-event", self.on_key_release)
self.connect("focus-in-event", self.on_focus_in)
self.connect("focus-out-event", self.on_focus_out)
# signal handlers
def on_enter_notify(self, widget, event):
pass
def on_leave_notify(self, widget, event):
pass
def on_button_press(self, widget, event):
pass
def on_button_release(self, widget, event):
star_index = self.get_star_at_xy(event.x, event.y)
if star_index is None:
return
if self.get_rating() == 1 and star_index == 1:
star_index = 0
self.set_rating(star_index)
self.emit('changed')
def on_key_press(self, widget, event):
pass
def on_key_release(self, widget, event):
pass
def on_focus_in(self, widget, event):
pass
def on_focus_out(self, widget, event):
pass
# public
def get_rating(self):
return self.rating
def render_star(self, widget, cr, x, y):
# paint focus
StarRenderer.render_star(self, widget, cr, x, y)
# if a star is hovered paint prelit star
def get_star_at_xy(self, x, y, half_star_precision=False):
star_width = self._size_map[self.size]()
star_index = x / star_width
remainder = 1.0
if half_star_precision:
if round((x % star_width) / star_width, 1) <= 0.5:
remainder = 0.5
if star_index > self.n_stars:
return None
return int(star_index) + remainder