Skip to content


Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 97cbe192dd
Fetching contributors…

Cannot retrieve contributors at this time

1108 lines (843 sloc) 41.396 kb
# - coding: utf-8 -
# Copyright (C) 2008-2010 Toms Bauģis <toms.baugis at>
# Dual licensed under the MIT or GPL Version 2 licenses.
# See
import math
import datetime as dt
import gtk, gobject
import pango, cairo
import re
import pytweener
except: # we can also live without tweener. Scene.animate will not work
pytweener = None
import colorsys
from collections import deque
class Colors(object):
hex_color_normal = re.compile("#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})")
hex_color_short = re.compile("#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])")
hex_color_long = re.compile("#([a-fA-F0-9]{4})([a-fA-F0-9]{4})([a-fA-F0-9]{4})")
def parse(self, color):
assert color is not None
#parse color into rgb values
if isinstance(color, basestring):
match = self.hex_color_long.match(color)
if match:
color = [int(color, 16) / 65535.0 for color in match.groups()]
match = self.hex_color_normal.match(color)
if match:
color = [int(color, 16) / 255.0 for color in match.groups()]
match = self.hex_color_short.match(color)
color = [int(color + color, 16) / 255.0 for color in match.groups()]
elif isinstance(color, gtk.gdk.Color):
color = [ / 65535.0, / 65535.0, / 65535.0]
# otherwise we assume we have color components in 0..255 range
if color[0] > 1 or color[1] > 1 or color[2] > 1:
color = [c / 255.0 for c in color]
return color
def rgb(self, color):
return [c * 255 for c in self.parse(color)]
def gdk(self, color):
c = self.parse(color)
return gtk.gdk.Color(int(c[0] * 65535.0), int(c[1] * 65535.0), int(c[2] * 65535.0))
def is_light(self, color):
# tells you if color is dark or light, so you can up or down the
# scale for improved contrast
return colorsys.rgb_to_hls(*self.rgb(color))[1] > 150
def darker(self, color, step):
# returns color darker by step (where step is in range 0..255)
hls = colorsys.rgb_to_hls(*self.rgb(color))
return colorsys.hls_to_rgb(hls[0], hls[1] - step, hls[2])
def contrast(self, color, step):
"""if color is dark, will return a lighter one, otherwise darker"""
hls = colorsys.rgb_to_hls(*self.rgb(color))
if self.is_light(color):
return colorsys.hls_to_rgb(hls[0], hls[1] - step, hls[2])
return colorsys.hls_to_rgb(hls[0], hls[1] + step, hls[2])
# returns color darker by step (where step is in range 0..255)
Colors = Colors() # this is a static class, so an instance will do
class Graphics(object):
"""If context is given upon contruction, will perform drawing
operations on context instantly. Otherwise queues up the drawing
instructions and performs them in passed-in order when _draw is called
with context.
Most of instructions are mapped to cairo functions by the same name.
Where there are differences, documenation is provided.
for detailed description of the cairo drawing functions.
def __init__(self, context = None):
self.context = context
self.colors = Colors # pointer to the color utilities instance
self.extents = None # bounds of the object, only if interactive
self.opacity = 1.0 # opacity get's adjusted by parent - TODO - wrong inheritance?
self.paths = None # paths for mouse hit checks
self._last_matrix = None
self.__instructions = deque() # paths colors and operations
self.__path_instructions = deque() # instruction set until it is converted into path-based instructions
def clear(self):
"""clear all instructions"""
self.__path_instructions = deque()
self.__instructions = deque()
self.paths = []
def _stroke(context): context.stroke()
def stroke(self, color = None, alpha = 1):
"""stroke the line with given color and opacity"""
if color or alpha < 1:self.set_color(color, alpha)
def _fill(context): context.fill()
def fill(self, color = None, alpha = 1):
"""fill path with given color and opacity"""
if color or alpha < 1:self.set_color(color, alpha)
def _stroke_preserve(context): context.stroke_preserve()
def stroke_preserve(self, color = None, alpha = 1):
"""same as stroke, only after stroking, don't discard the path"""
if color or alpha < 1:self.set_color(color, alpha)
def _fill_preserve(context): context.fill_preserve()
def fill_preserve(self, color = None, alpha = 1):
"""same as fill, only after filling, don't discard the path"""
if color or alpha < 1:self.set_color(color, alpha)
def _new_path(context): context.new_path()
def new_path(self):
"""discard current path"""
def _paint(context): context.paint()
def paint(self):
"""errrm. paint"""
def _set_source_surface(context, image, x, y):
context.set_source_surface(image, x, y)
def set_source_surface(self, image, x = 0, y = 0):
self._add_instruction(self._set_source_surface, image, x, y)
def _set_source_pixbuf(context, pixbuf, x, y):
context.set_source_pixbuf(pixbuf, x, y)
def set_source_pixbuf(self, pixbuf, x = 0, y = 0):
self._add_instruction(self._set_source_pixbuf, pixbuf, x, y)
def _move_to(context, x, y): context.move_to(x, y)
def move_to(self, x, y):
"""change current position"""
self._add_instruction(self._move_to, x, y)
def _line_to(context, x, y): context.line_to(x, y)
def line_to(self, x, y):
"""draw line"""
self._add_instruction(self._line_to, x, y)
def _rel_line_to(context, x, y): context.rel_line_to(x, y)
def rel_line_to(self, x, y):
"""draw line"""
self._add_instruction(self._rel_line_to, x, y)
def _curve_to(context, x, y, x2, y2, x3, y3):
context.curve_to(x, y, x2, y2, x3, y3)
def curve_to(self, x, y, x2, y2, x3, y3):
"""draw curve. (x2, y2) is the middle point of the curve"""
self._add_instruction(self._curve_to, x, y, x2, y2, x3, y3)
def _close_path(context): context.close_path()
def close_path(self):
"""connect end with beginning of path"""
def _set_line_width(context, width):
def _set_dash(context, dash, dash_offset = 0):
context.set_dash(dash, dash_offset)
def set_line_style(self, width = None, dash = None, dash_offset = 0):
"""change the width of the line"""
if width is not None:
self._add_instruction(self._set_line_width, width)
if dash is not None:
self._add_instruction(self._set_dash, dash, dash_offset)
def _set_color(self, context, r, g, b, a):
if a * self.opacity >= 1:
context.set_source_rgb(r, g, b)
context.set_source_rgba(r, g, b, a * self.opacity)
def set_color(self, color, alpha = 1):
"""set active color. You can use hex colors like "#aaa", or you can use
normalized RGB tripplets (where every value is in range 0..1), or
you can do the same thing in range 0..65535"""
color = self.colors.parse(color) # parse whatever we have there into a normalized triplet
if len(color) == 4 and alpha is None:
alpha = color[3]
r, g, b = color[:3]
self._add_instruction(self._set_color, r, g, b, alpha)
def _arc(context, x, y, radius, start_angle, end_angle):
context.arc(x, y, radius, start_angle, end_angle)
def arc(self, x, y, radius, start_angle, end_angle):
"""draw arc going counter-clockwise from start_angle to end_angle"""
self._add_instruction(self._arc, x, y, radius, start_angle, end_angle)
def circle(self, x, y, radius):
"""draw circle"""
self._add_instruction(self._arc, x, y, radius, 0, math.pi * 2)
def ellipse(self, x, y, width, height, edges = None):
"""draw 'perfect' ellipse, opposed to squashed circle. works also for
equilateral polygons"""
# the automatic edge case is somewhat arbitrary
steps = edges or max((32, width, height)) / 2
angle = 0
step = math.pi * 2 / steps
points = []
while angle < math.pi * 2:
points.append((width / 2.0 * math.cos(angle),
height / 2.0 * math.sin(angle)))
angle += step
min_x = min((point[0] for point in points))
min_y = min((point[1] for point in points))
self.move_to(points[0][0] - min_x + x, points[0][1] - min_y + y)
for p_x, p_y in points:
self.line_to(p_x - min_x + x, p_y - min_y + y)
self.line_to(points[0][0] - min_x + x, points[0][1] - min_y + y)
def _arc_negative(context, x, y, radius, start_angle, end_angle):
context.arc_negative(x, y, radius, start_angle, end_angle)
def arc_negative(self, x, y, radius, start_angle, end_angle):
"""draw arc going clockwise from start_angle to end_angle"""
self._add_instruction(self._arc_negative, x, y, radius, start_angle, end_angle)
def _rounded_rectangle(context, x, y, x2, y2, corner_radius):
half_corner = corner_radius / 2
context.move_to(x + corner_radius, y)
context.line_to(x2 - corner_radius, y)
context.curve_to(x2 - half_corner, y, x2, y + half_corner, x2, y + corner_radius)
context.line_to(x2, y2 - corner_radius)
context.curve_to(x2, y2 - half_corner, x2 - half_corner, y2, x2 - corner_radius, y2)
context.line_to(x + corner_radius, y2)
context.curve_to(x + half_corner, y2, x, y2 - half_corner, x, y2 - corner_radius)
context.line_to(x, y + corner_radius)
context.curve_to(x, y + half_corner, x + half_corner, y, x + corner_radius, y)
def _rectangle(context, x, y, w, h): context.rectangle(x, y, w, h)
def rectangle(self, x, y, width, height, corner_radius = 0):
"draw a rectangle. if corner_radius is specified, will draw rounded corners"
if corner_radius <= 0:
self._add_instruction(self._rectangle, x, y, width, height)
# make sure that w + h are larger than 2 * corner_radius
corner_radius = min(corner_radius, min(width, height) / 2)
x2, y2 = x + width, y + height
self._add_instruction(self._rounded_rectangle, x, y, x2, y2, corner_radius)
def fill_area(self, x, y, width, height, color, opacity = 1):
"""fill rectangular area with specified color"""
self.rectangle(x, y, width, height)
self.fill(color, opacity)
def fill_stroke(self, fill = None, stroke = None, line_width = None):
if line_width: self.set_line_style(line_width)
if fill and stroke:
elif fill:
if stroke:
def _show_layout(context, text, font_desc, alignment, width, wrap, ellipsize):
layout = context.create_layout()
if width > 0:
if wrap is not None:
layout.set_ellipsize(ellipsize or pango.ELLIPSIZE_END)
def create_layout(self, size = None):
"""utility function to create layout with the default font. Size and
alignment parameters are shortcuts to according functions of the
if not self.context:
# TODO - this is rather sloppy as far as exception goes
# should explain better
raise "Can not create layout without existing context!"
layout = self.context.create_layout()
font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
if size: font_desc.set_size(size * pango.SCALE)
return layout
def show_text(self, text, size = None, color = None):
"""display text with system's default font"""
font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
if color: self.set_color(color)
if size: font_desc.set_size(size * pango.SCALE)
self.show_layout(text, font_desc)
def show_layout(self, text, font_desc, alignment = pango.ALIGN_LEFT, width = -1, wrap = None, ellipsize = None):
"""display text. font_desc is string of pango font description
often handier than calling this function directly, is to create
a class:Label object
self._add_instruction(self._show_layout, text, font_desc, alignment, width, wrap, ellipsize)
def _remember_path(self, context):
new_extents = context.path_extents()
self.extents = self.extents or new_extents
self.extents = (min(self.extents[0], new_extents[0]),
min(self.extents[1], new_extents[1]),
max(self.extents[2], new_extents[2]),
max(self.extents[3], new_extents[3]))
def _add_instruction(self, function, *params):
if self.context:
function(self.context, *params)
self.paths = None
self.__path_instructions.append((function, params))
def _draw(self, context, with_extents = False):
"""draw accumulated instructions in context"""
if self.__path_instructions: #new stuff!
self.__instructions = deque()
current_color = None
current_line = None
instruction_cache = []
while self.__path_instructions:
instruction, args = self.__path_instructions.popleft()
if instruction in (self._set_source_surface, self._set_source_pixbuf, self._paint):
self.__instructions.append((None, None, None, instruction, args))
elif instruction == self._show_layout:
self.__instructions.append((None, current_color, None, instruction, args))
elif instruction == self._set_color:
current_color = args
elif instruction == self._set_line_width:
current_line = args
elif instruction in (self._stroke, self._fill,
instruction, ()))
context.new_path() # reset even on preserve as the instruction will preserve it instead
instruction_cache = []
# the rest are non-special
instruction(context, *args)
instruction_cache.append((instruction, args))
while instruction_cache: # stroke is missing so we just cache
instruction, args = instruction_cache.pop(0)
self.__instructions.append((None, None, None, instruction, args))
# if we have been moved around, we should update bounds
check_extents = with_extents and (context.get_matrix() != self._last_matrix or not self.paths)
if check_extents:
self.paths = deque()
self.extents = None
for path, color, line, instruction, args in self.__instructions:
if color: self._set_color(context, *color)
if line: self._set_line_width(context, *line)
if path:
if check_extents:
if instruction:
instruction(context, *args)
self._last_matrix = context.get_matrix()
class Sprite(gtk.Object):
"""The Sprite class is a basic display list building block: a display list
node that can display graphics and can also contain children.
Once you have created the sprite, use Scene's add_child to add it to
__gsignals__ = {
"on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
"on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
"on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-render": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
#"on-draw": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
def __init__(self, x = 0, y = 0,
opacity = 1, visible = True,
rotation = 0, pivot_x = 0, pivot_y = 0,
scale_x = 1, scale_y = 1,
interactive = False, draggable = False,
z_order = 0):
#: list of children sprites. Use :func:`add_child` to add sprites
self.sprites = []
#: instance of :ref:`graphics` for this sprite = Graphics()
#: boolean denoting whether the sprite responds to mouse events
self.interactive = interactive
#: boolean marking if sprite can be automatically dragged
self.draggable = draggable
#: relative coordinates of the sprites anchor and rotation point
self.pivot_x, self.pivot_y = pivot_x, pivot_y # rotation point in sprite's coordinates
#: sprite opacity
self.opacity = opacity
#: boolean visibility flag
self.visible = visible
#: pointer to parent :class:`Sprite` or :class:`Scene`
self.parent = None
#: sprite coordinates
self.x, self.y = x, y
#: rotation of the sprite in radians (use :func:`math.degrees` to convert to degrees if necessary)
self.rotation = rotation
#: scale X
self.scale_x = scale_x
#: scale Y
self.scale_y = scale_y
#: drawing order between siblings. The one with the highest z_order will be on top.
self.z_order = z_order
self.__dict__["_sprite_dirty"] = True # flag that indicates that the graphics object of the sprite should be rendered
def __setattr__(self, name, val):
if self.__dict__.get(name, "hamster_graphics_no_value_really") != val:
self.__dict__[name] = val
if name not in ('x', 'y', 'rotation', 'scale_x', 'scale_y'):
self.__dict__["_sprite_dirty"] = True
def add_child(self, *sprites):
"""Add child sprite. Child will be nested within parent"""
for sprite in sprites:
sprite.parent = self
self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
def _draw(self, context, opacity = 1):
if self.visible is False:
if any([self.x, self.y, self.rotation, self.scale_x, self.scale_y]):
if self.x or self.y or self.pivot_x or self.pivot_y:
context.translate(self.x + self.pivot_x, self.y + self.pivot_y)
if self.rotation:
if self.pivot_x or self.pivot_y:
context.translate(-self.pivot_x, -self.pivot_y)
if self.scale_x != 1 or self.scale_y != 1:
context.scale(self.scale_x, self.scale_y) = self.opacity * opacity
#self.emit("on-draw") # TODO - this is expensive when doing constant redraw with many frames. maybe we can have a simple callback here?, 0, 0) # TODO - i'm doing this move because otherwise the currentpoint is pointing to wherever it was left. check if we really need this
if (self._sprite_dirty): # send signal to redo the drawing when sprite is dirty
self.__dict__["_sprite_dirty"] = False, self.interactive or self.draggable)
for sprite in self.sprites:
sprite._draw(context, self.opacity * opacity)
if any([self.x, self.y, self.rotation, self.scale_x, self.scale_y]):
def _on_click(self, button_state):
self.emit("on-click", button_state)
if self.parent and isinstance(self.parent, Sprite):
def _on_mouse_over(self):
# scene will call us when there is mouse
def _on_mouse_out(self):
# scene will call us when there is mouse
def _on_drag(self, x, y):
# scene will call us when there is mouse
self.emit("on-drag", (x, y))
class Label(Sprite):
def __init__(self, text = "", size = 10, color = None,
alignment = pango.ALIGN_LEFT, **kwargs):
Sprite.__init__(self, **kwargs)
self.width, self.height = None, None
#: pango.FontDescription, default is the system's font
self.font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
self.font_desc.set_size(size * pango.SCALE)
#: color of label either as hex string or an (r,g,b) tuple
self.color = color
self._bounds_width = -1
#: wrapping method. Can be set to pango. [WRAP_WORD, WRAP_CHAR,
self.wrap = None
#: Ellipsize mode. Can be set to pango. [ELLIPSIZE_NONE,
self.ellipsize = None
#: alignment. one of pango.[ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER]
self.alignment = alignment
#: label text
self.text = text
#: font size
self.size = size
self.connect("on-render", self.on_render)
def __setattr__(self, name, val):
Sprite.__setattr__(self, name, val)
if name == "width":
# setting width means consumer wants to contrain the label
if val is None or val == -1:
self.__dict__['_bounds_width'] = -1
self.__dict__['_bounds_width'] = val * pango.SCALE
if name in ("width", "text", "size", "font_desc", "wrap", "ellipsize"):
# avoid chicken and egg
if "text" in self.__dict__ and "size" in self.__dict__ and "width" in self.__dict__:
def on_render(self, sprite):
if not self.text:
if self.interactive: #if label is interactive, draw invisible bounding box for simple hit calculations"#000", 0),0, self.width, self.height), self.font_desc,
def _set_dimensions(self):
context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)))
layout = context.create_layout()
if self.wrap is not None:
layout.set_ellipsize(self.ellipsize or pango.ELLIPSIZE_END)
# TODO - the __dict__ part look rather lame but allows to circumvent the setattr
self.__dict__['width'], self.height = layout.get_pixel_size()
class Rectangle(Sprite):
def __init__(self, w, h, corner_radius = 0, fill = None, stroke = None, **kwargs):
Sprite.__init__(self, **kwargs)
#: width
self.width = w
#: height
self.height = h
#: fill color
self.fill = fill
#: stroke color
self.stroke = stroke
#: stroke line width
self.line_width = 1
#: corner radius. Set bigger than 0 for rounded corners
self.corner_radius = corner_radius
self.connect("on-render", self.on_render)
def on_render(self, sprite):, 0, self.width, self.height, self.corner_radius), self.stroke, self.line_width)
class Polygon(Sprite):
def __init__(self, points, fill = None, stroke = None, line_width = 1, **kwargs):
Sprite.__init__(self, **kwargs)
#: list of (x,y) tuples that the line should go through. Polygon
#: will automatically close path.
self.points = points
#: fill color
self.fill = fill
#: stroke color
self.stroke = stroke
#: stroke line width
self.line_width = line_width
self.connect("on-render", self.on_render)
def on_render(self, sprite):
if not self.points: return*self.points[0])
for point in self.points:*point), self.stroke, self.line_width)
class Circle(Sprite):
def __init__(self, width, height, fill = None, stroke = None, line_width = 1, **kwargs):
Sprite.__init__(self, **kwargs)
#: circle width
self.width = width
#: circle height
self.height = height
#: fill color
self.fill = fill
#: stroke color
self.stroke = stroke
#: stroke line width
self.line_width = line_width
self.connect("on-render", self.on_render)
def on_render(self, sprite):
if self.width == self.height:, self.width / 2.0, self.width / 2.0)
else:, 0, self.width, self.height), self.stroke, self.line_width)
class Scene(gtk.DrawingArea):
""" Widget for displaying sprites.
Add sprites to the Scene by calling :func:`add_child`.
Scene is descendant of `gtk.DrawingArea <>`_
and thus inherits all it's methods and everything.
__gsignals__ = {
#: yes can haz instance attribute docstring
"expose-event": "override",
"configure_event": "override",
"on-enter-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
"on-finish-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
"on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
"on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
"on-mouse-move": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-mouse-down": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-mouse-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
"on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-scroll": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
def __init__(self, interactive = True, framerate = 80):
if interactive:
| gtk.gdk.SCROLL_MASK)
self.connect("motion_notify_event", self.__on_mouse_move)
self.connect("enter_notify_event", self.__on_mouse_enter)
self.connect("leave_notify_event", self.__on_mouse_leave)
self.connect("button_press_event", self.__on_button_press)
self.connect("button_release_event", self.__on_button_release)
self.connect("scroll-event", self.__on_scroll)
#: list of sprites in scene. use :func:`add_child` to add sprites
self.sprites = []
#: framerate of animation. This will limit how often call for
#: redraw will be performed (that is - not more often than the framerate). It will
#: also influence the smoothness of tweeners.
self.framerate = framerate
#: width and height of the scene. Will be `None` until first
#: expose (that is until first on-enter-frame signal below).
self.width, self.height = None, None
#: instance of :class:`pytweener.Tweener` that is used by
#: :func:`animate` function, but can be also accessed directly for advanced control.
self.tweener = None
if pytweener:
self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.ease_in_out)
#: instance of :class:`Colors` class for color parsing
self.colors = Colors
#: last known coordinates of mouse cursor
self.mouse_x, self.mouse_y = None, None
#: read only info about current framerate (frames per second)
self.fps = 0 # inner frames per second counter
#: Mouse cursor appearance.
#: Replace with your own cursor or set to False to have no cursor.
#: None will revert back the default behavior
self.mouse_cursor = None
blank_pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
self._blank_cursor = gtk.gdk.Cursor(blank_pixmap, blank_pixmap, gtk.gdk.Color(), gtk.gdk.Color(), 0, 0)
self._last_frame_time = None
self._mouse_sprite = None
self._mouse_drag = None
self._drag_sprite = None
self._button_press_time = None # to distinguish between click and drag
self._mouse_in = False
self.__drawing_queued = False
self.__drag_x, self.__drag_y = None, None
self.__last_expose_time =
def add_child(self, *sprites):
"""Add one or several :class:`graphics.Sprite` sprites to scene """
for sprite in sprites:
self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
def clear(self):
"""Remove all sprites from scene"""
self.sprites = []
def redraw(self):
"""Queue redraw. The redraw will be performed not more often than
the `framerate` allows"""
if self.__drawing_queued == False: #if we are moving, then there is a timeout somewhere already
self.__drawing_queued = True
self._last_frame_time =
gobject.timeout_add(1000 / self.framerate, self.__interpolate)
# animation bits
def __interpolate(self):
if self.tweener:
self.tweener.update(( - self._last_frame_time).microseconds / 1000000.0)
self.__drawing_queued = self.tweener.has_tweens()
self._last_frame_time =
self.queue_draw() # this will trigger do_expose_event when the current events have been flushed
return self.__drawing_queued
def animate(self, sprite, duration = None, easing = None, on_complete = None, on_update = None, delay = None, **kwargs):
"""Interpolate attributes of the given object using the internal tweener
and redrawing scene after every tweener update.
Specify the sprite and sprite's attributes that need changing.
`duration` defaults to 0.4 seconds and `easing` to cubic in-out
(for others see pytweener.Easing class).
Redraw is requested right after creating the animation.
# tween some_sprite to coordinates (50,100) using default duration and easing
scene.animate(some_sprite, x = 50, y = 100)
if not self.tweener: # here we complain
raise Exception("pytweener was not found. Include it to enable animations")
tween = self.tweener.add_tween(sprite,
delay=delay, **kwargs)
return tween
# exposure events
def do_configure_event(self, event):
self.width, self.height = event.width, event.height
def do_expose_event(self, event):
context = self.window.cairo_create()
# clip to the visible part
context.rectangle(event.area.x, event.area.y,
event.area.width, event.area.height)
now =
self.fps = 1 / ((now - self.__last_expose_time).microseconds / 1000000.0)
self.__last_expose_time = now
self.emit("on-enter-frame", context)
for sprite in self.sprites:
self._check_mouse(self.mouse_x, self.mouse_y)
self.emit("on-finish-frame", context)
""" mouse events """
def all_sprites(self, sprites = None):
"""returns flat list of the sprite tree for simplified iteration"""
if sprites is None:
sprites = self.sprites
for sprite in sprites:
yield sprite
if sprite.sprites:
for child in self.all_sprites(sprite.sprites):
yield child
def __on_scroll(self, area, event):
self.emit("on-scroll", event)
def __on_mouse_move(self, area, event):
mouse_x = event.x
mouse_y = event.y
state = event.state
self.mouse_x, self.mouse_y = mouse_x, mouse_y
if self._drag_sprite and self._drag_sprite.draggable \
and gtk.gdk.BUTTON1_MASK & event.state:
# dragging around
drag = self._mouse_drag \
and (self._mouse_drag[0] - event.x) ** 2 + \
(self._mouse_drag[1] - event.y) ** 2 > 5 ** 2
if drag:
matrix = cairo.Matrix()
if self._drag_sprite.parent:
# TODO - this currently works only until second level
# should take all parents into account
if not self.__drag_x:
x1,y1 = matrix.transform_point(self._mouse_drag[0],
self.__drag_x = self._drag_sprite.x - x1
self.__drag_y = self._drag_sprite.y - y1
mouse_x, mouse_y = matrix.transform_point(mouse_x, mouse_y)
new_x = mouse_x + self.__drag_x
new_y = mouse_y + self.__drag_y
self._drag_sprite.x, self._drag_sprite.y = new_x, new_y
self._drag_sprite._on_drag(new_x, new_y)
self.emit("on-drag", self._drag_sprite, (new_x, new_y))
# avoid double mouse checks - the redraw will also check for mouse!
if not self.__drawing_queued:
self._check_mouse(event.x, event.y)
self.emit("on-mouse-move", event)
def _check_mouse(self, mouse_x, mouse_y):
if mouse_x is None or self._mouse_in == False:
custom_mouse = self.mouse_cursor is not None
cursor = gtk.gdk.ARROW
if custom_mouse:
if self.mouse_cursor == False:
cursor = self._blank_cursor
cursor = self.mouse_cursor
#check if we have a mouse over
over = None
for sprite in self.all_sprites():
if sprite.interactive and sprite.visible and self._check_hit(sprite, mouse_x, mouse_y):
over = sprite
if over:
if custom_mouse == False:
if over.draggable:
cursor = gtk.gdk.FLEUR
cursor = gtk.gdk.HAND2
if over != self._mouse_sprite:
self.emit("on-mouse-over", over)
if self._mouse_sprite and self._mouse_sprite != over:
self.emit("on-mouse-out", self._mouse_sprite)
self._mouse_sprite = over
if isinstance(cursor, gtk.gdk.Cursor):
def __on_mouse_enter(self, area, event):
self._mouse_in = True
def __on_mouse_leave(self, area, event):
self._mouse_in = False
if self._mouse_sprite:
self.emit("on-mouse-out", self._mouse_sprite)
self._mouse_sprite = None
def _check_hit(self, sprite, x, y):
if sprite == self._drag_sprite:
return True
if not
return False
sprite_x, sprite_y, sprite_x2, sprite_y2 =
if sprite_x <= x <= sprite_x2 and sprite_y <= y <= sprite_y2:
paths =
if not paths:
return True
context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, self.width, self.height))
for path in paths:
return context.in_fill(x, y)
return False
def __on_button_press(self, area, event):
x = event.x
y = event.y
state = event.state
self._mouse_drag = (x, y)
over = None
for sprite in self.all_sprites():
if sprite.interactive and sprite.visible and self._check_hit(sprite, event.x, event.y):
over = sprite # last one will take precedence
self._drag_sprite = over
self._button_press_time =
self.emit("on-mouse-down", event)
def __on_button_release(self, area, event):
#if the drag is less than 5 pixles, then we have a click
click = self._button_press_time \
and ( - self._button_press_time) < dt.timedelta(milliseconds = 200) \
and (event.x - self._mouse_drag[0]) ** 2 + (event.y - self._mouse_drag[1]) ** 2 < 60
self._button_press_time = None
self._mouse_drag = None
self.__drag_x, self.__drag_y = None, None
self._drag_sprite = None
if click:
target = None
for sprite in self.all_sprites():
if sprite.interactive and sprite.visible and self._check_hit(sprite, event.x, event.y):
target = sprite
if target:
self.emit("on-click", event, target)
Jump to Line
Something went wrong with that request. Please try again.