[hamster-applet] sync with hamster_experiments
- From: Toms Baugis <tbaugis src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [hamster-applet] sync with hamster_experiments
- Date: Fri, 10 Sep 2010 22:54:40 +0000 (UTC)
commit 0ef472f72179bc90f371f8c6acdedb5b9ef938c4
Author: Toms Bauģis <toms baugis gmail com>
Date: Fri Sep 10 23:52:47 2010 +0100
sync with hamster_experiments
src/hamster/utils/graphics.py | 737 ++++++++++++++++++++++++++++++-----------
1 files changed, 540 insertions(+), 197 deletions(-)
---
diff --git a/src/hamster/utils/graphics.py b/src/hamster/utils/graphics.py
index 4a6c5df..0c86f1e 100644
--- a/src/hamster/utils/graphics.py
+++ b/src/hamster/utils/graphics.py
@@ -99,13 +99,14 @@ class Graphics(object):
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
+ self.__new_instructions = deque() # instruction set until it is converted into path-based instructions
+ self.__instruction_cache = None
+ self.cache_surface = None
def clear(self):
"""clear all instructions"""
- self.__path_instructions = deque()
- self.__instructions = deque()
+ self.__new_instructions = deque()
+ self.__instruction_cache = None
self.paths = []
@staticmethod
@@ -149,10 +150,16 @@ class Graphics(object):
self._add_instruction(self._paint,)
@staticmethod
- 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(context, image):
+ context.set_source(image)
+ def set_source(self, image, x = 0, y = 0):
+ self._add_instruction(self._set_source, image)
+
+ @staticmethod
+ def _set_source_surface(context, surface, x, y):
+ context.set_source_surface(surface, x, y)
+ def set_source_surface(self, surface, x = 0, y = 0):
+ self._add_instruction(self._set_source_surface, surface, x, y)
@staticmethod
def _set_source_pixbuf(context, pixbuf, x, y):
@@ -160,6 +167,30 @@ class Graphics(object):
def set_source_pixbuf(self, pixbuf, x = 0, y = 0):
self._add_instruction(self._set_source_pixbuf, pixbuf, x, y)
+ @staticmethod
+ def _save_context(context): context.save()
+ def save_context(self):
+ """change current position"""
+ self._add_instruction(self._save_context)
+
+ @staticmethod
+ def _restore_context(context): context.restore()
+ def restore_context(self):
+ """change current position"""
+ self._add_instruction(self._restore_context)
+
+
+ @staticmethod
+ def _translate(context, x, y): context.translate(x, y)
+ def translate(self, x, y):
+ """change current position"""
+ self._add_instruction(self._translate, x, y)
+
+ @staticmethod
+ def _rotate(context, radians): context.rotate(radians)
+ def rotate(self, radians):
+ """change current position"""
+ self._add_instruction(self._rotate, radians)
@staticmethod
def _move_to(context, x, y): context.move_to(x, y)
@@ -169,15 +200,25 @@ class Graphics(object):
@staticmethod
def _line_to(context, x, y): context.line_to(x, y)
- def line_to(self, x, y):
+ def line_to(self, x, y = None):
"""draw line"""
- self._add_instruction(self._line_to, x, y)
+ if y is not None:
+ self._add_instruction(self._line_to, x, y)
+ elif isinstance(x, list) and y is None:
+ for x2, y2 in x:
+ self._add_instruction(self._line_to, x2, y2)
+
@staticmethod
def _rel_line_to(context, x, y): context.rel_line_to(x, y)
- def rel_line_to(self, x, y):
+ def rel_line_to(self, x, y = None):
"""draw line"""
- self._add_instruction(self._rel_line_to, x, y)
+ if x and y:
+ self._add_instruction(self._rel_line_to, x, y)
+ elif isinstance(x, list) and y is None:
+ for x2, y2 in x:
+ self._add_instruction(self._rel_line_to, x2, y2)
+
@staticmethod
def _curve_to(context, x, y, x2, y2, x3, y3):
@@ -356,11 +397,15 @@ class Graphics(object):
"""
self._add_instruction(self._show_layout, text, font_desc, alignment, width, wrap, ellipsize)
- def _remember_path(self, context):
+ def _remember_path(self, context, instruction):
context.save()
context.identity_matrix()
- new_extents = context.path_extents()
+ if instruction in (self._fill, self._fill_preserve):
+ new_extents = context.path_extents()
+ else:
+ new_extents = context.stroke_extents()
+
self.extents = self.extents or new_extents
self.extents = (min(self.extents[0], new_extents[0]),
min(self.extents[1], new_extents[1]),
@@ -377,26 +422,31 @@ class Graphics(object):
function(self.context, *params)
else:
self.paths = None
- self.__path_instructions.append((function, params))
+ self.__new_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()
+ if self.__new_instructions: #new stuff!
+ self.__instruction_cache = deque()
current_color = None
current_line = None
instruction_cache = []
- while self.__path_instructions:
- instruction, args = self.__path_instructions.popleft()
+ while self.__new_instructions:
+ instruction, args = self.__new_instructions.popleft()
- if instruction in (self._set_source_surface, self._set_source_pixbuf, self._paint):
- self.__instructions.append((None, None, None, instruction, args))
+ if instruction in (self._set_source,
+ self._set_source_surface,
+ self._set_source_pixbuf,
+ self._paint,
+ self._translate,
+ self._save_context,
+ self._restore_context):
+ self.__instruction_cache.append((None, None, None, instruction, args))
elif instruction == self._show_layout:
- self.__instructions.append((None, current_color, None, instruction, args))
+ self.__instruction_cache.append((None, current_color, None, instruction, args))
elif instruction == self._set_color:
current_color = args
@@ -404,10 +454,10 @@ class Graphics(object):
elif instruction == self._set_line_width:
current_line = args
- elif instruction in (self._stroke, self._fill,
+ elif instruction in (self._new_path, self._stroke, self._fill,
self._stroke_preserve,
self._fill_preserve):
- self.__instructions.append((context.copy_path(),
+ self.__instruction_cache.append((context.copy_path(),
current_color,
current_line,
instruction, ()))
@@ -421,7 +471,7 @@ class Graphics(object):
while instruction_cache: # stroke is missing so we just cache
instruction, args = instruction_cache.pop(0)
- self.__instructions.append((None, None, None, instruction, args))
+ self.__instruction_cache.append((None, None, None, instruction, args))
# if we have been moved around, we should update bounds
@@ -430,22 +480,99 @@ class Graphics(object):
self.paths = deque()
self.extents = None
- for path, color, line, instruction, args in self.__instructions:
+ if not self.__instruction_cache:
+ return
+
+ for path, color, line, instruction, args in self.__instruction_cache:
if color: self._set_color(context, *color)
if line: self._set_line_width(context, *line)
if path:
context.append_path(path)
if check_extents:
- self._remember_path(context)
+ self._remember_path(context, self._fill)
if instruction:
- instruction(context, *args)
+ if instruction == self._paint and self.opacity < 1:
+ context.paint_with_alpha(self.opacity)
+ else:
+ instruction(context, *args)
+
+ if check_extents and instruction not in (self._fill, self._stroke, self._fill_preserve, self._stroke_preserve):
+ # last one
+ self._remember_path(context, self._fill)
self._last_matrix = context.get_matrix()
+ def _draw_as_bitmap(self, context):
+ """
+ instead of caching paths, this function caches the whole drawn thing
+ use cache_as_bitmap on sprite to enable this mode
+ """
+ matrix = context.get_matrix()
+
+ if self.__new_instructions or matrix != self._last_matrix:
+ if self.__new_instructions:
+ self.__instruction_cache = list(self.__new_instructions)
+ self.__new_instructions = deque()
+
+ self.paths = deque()
+ self.extents = None
+
+ if not self.__instruction_cache:
+ # no instructions - nothing to do
+ return
+
+ # instructions that end path
+ path_end_instructions = (self._new_path, self._stroke, self._fill, self._stroke_preserve, self._fill_preserve)
+
+ # measure the path extents so we know the size of surface
+ for instruction, args in self.__instruction_cache:
+ if instruction in path_end_instructions:
+ self._remember_path(context, instruction)
+
+ if instruction in (self._set_source_pixbuf, self._set_source_surface):
+ # draw a rectangle around the pathless instructions so that the extents are correct
+ pixbuf = args[0]
+ x = args[1] if len(args) > 1 else 0
+ y = args[2] if len(args) > 2 else 0
+ self._rectangle(context, x, y, pixbuf.get_width(), pixbuf.get_height())
+
+ instruction(context, *args)
+
+ if instruction not in path_end_instructions: # last one
+ self._remember_path(context, self._fill)
+
+ # now draw the instructions on the caching surface
+ w = int(self.extents[2] - self.extents[0]) + 1
+ h = int(self.extents[3] - self.extents[1]) + 1
+ self.cache_surface = context.get_target().create_similar(cairo.CONTENT_COLOR_ALPHA, w, h)
+ ctx = gtk.gdk.CairoContext(cairo.Context(self.cache_surface))
+ ctx.translate(-self.extents[0], -self.extents[1])
+
+ ctx.transform(matrix)
+ for instruction, args in self.__instruction_cache:
+ instruction(ctx, *args)
+
+ self._last_matrix = matrix
+ else:
+ context.save()
+ context.identity_matrix()
+ context.translate(self.extents[0], self.extents[1])
+ context.set_source_surface(self.cache_surface)
+
+ if self.opacity < 1:
+ context.paint_with_alpha(self.opacity)
+ else:
+ context.paint()
+ context.restore()
+
+
+
+
+
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.
@@ -457,16 +584,17 @@ class Sprite(gtk.Object):
"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-start": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-drag-finish": (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):
+ z_order = 0, cache_as_bitmap = False, mouse_cursor = None):
gtk.Object.__init__(self)
#: list of children sprites. Use :func:`add_child` to add sprites
@@ -508,31 +636,119 @@ class Sprite(gtk.Object):
#: drawing order between siblings. The one with the highest z_order will be on top.
self.z_order = z_order
+ #: Whether the sprite should be cached as a bitmap. Default: true
+ #: Generally good when you have many static sprites
+ self.cache_as_bitmap = cache_as_bitmap
+
+ #: mouse-over cursor of the sprite. See :class:`Scene`.mouse_cursor
+ #: for possible values
+ self.mouse_cursor = mouse_cursor
+
+ #: x position of the cursor within mouse upon drag. change this value
+ #: in on-drag-start to adjust drag point
+ self.drag_x = None
+
+ #: y position of the cursor within mouse upon drag. change this value
+ #: in on-drag-start to adjust drag point
+ self.drag_y = None
+
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'):
+ if name not in ('x', 'y', 'rotation', 'scale_x', 'scale_y', 'visible'):
self.__dict__["_sprite_dirty"] = True
+ if name in ('x', 'y'):
+ val = int(val)
+
+ self.__dict__[name] = val
+ self.redraw()
def add_child(self, *sprites):
"""Add child sprite. Child will be nested within parent"""
for sprite in sprites:
+ if sprite.parent:
+ sprite.parent.remove_child(sprite)
+
self.sprites.append(sprite)
sprite.parent = self
self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
+ def remove_child(self, *sprites):
+ for sprite in sprites:
+ self.sprites.remove(sprite)
+ sprite.parent = None
+
+ def check_hit(self, x, y):
+ """check if the given coordinates are inside the sprite's fill or stroke
+ path"""
+ if not self.graphics.extents:
+ return False
+
+ sprite_x, sprite_y, sprite_x2, sprite_y2 = self.graphics.extents
+
+ if sprite_x <= x <= sprite_x2 and sprite_y <= y <= sprite_y2:
+ paths = self.graphics.paths
+ if not paths:
+ return True
+
+ context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0))
+ for path in paths:
+ context.append_path(path)
+ return context.in_fill(x, y)
+ else:
+ return False
+
+ def get_scene(self):
+ """returns class:`Scene` the sprite belongs to"""
+ if hasattr(self, 'parent') and self.parent:
+ if isinstance(self.parent, Scene):
+ return self.parent
+ else:
+ return self.parent.get_scene()
+ return None
+
+ def redraw(self):
+ """queue redraw of the sprite. this function is called automatically
+ whenever a sprite attribute changes. sprite changes that happen
+ during scene redraw are ignored in order to avoid echoes.
+ Call scene.redraw() explicitly if you need to redraw in these cases.
+ """
+ scene = self.get_scene()
+ if scene and scene._redraw_in_progress == False:
+ self.parent.redraw()
+
+ def animate(self, duration = None, easing = None, on_complete = None, on_update = None, delay = None, **kwargs):
+ """Request paretn Scene to Interpolate attributes using the internal tweener.
+ Specify sprite's attributes that need changing.
+ `duration` defaults to 0.4 seconds and `easing` to cubic in-out
+ (for others see pytweener.Easing class).
+
+ Example::
+ # tween some_sprite to coordinates (50,100) using default duration and easing
+ self.animate(x = 50, y = 100)
+ """
+ scene = self.get_scene()
+ if scene:
+ scene.animate(self, duration, easing, on_complete, on_update, delay, **kwargs)
+
def _draw(self, context, opacity = 1):
if self.visible is False:
return
- if any([self.x, self.y, self.rotation, self.scale_x, self.scale_y]):
+ context.new_path()
+
+ if (self._sprite_dirty): # send signal to redo the drawing when sprite is dirty
+ self.emit("on-render")
+ self.__dict__["_sprite_dirty"] = False
+
+
+ if any((self.x, self.y, self.rotation, self.scale_x, self.scale_y)):
context.save()
- if self.x or self.y or self.pivot_x or self.pivot_y:
+ if any((self.x, self.y, self.pivot_x, self.pivot_y)):
context.translate(self.x + self.pivot_x, self.y + self.pivot_y)
if self.rotation:
@@ -546,22 +762,19 @@ class Sprite(gtk.Object):
self.graphics.opacity = 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?
- #self.graphics._move_to(context, 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
- context.new_path()
-
- if (self._sprite_dirty): # send signal to redo the drawing when sprite is dirty
- self.emit("on-render")
- self.__dict__["_sprite_dirty"] = False
-
- self.graphics._draw(context, self.interactive or self.draggable)
+ if self.cache_as_bitmap:
+ self.graphics._draw_as_bitmap(context)
+ else:
+ self.graphics._draw(context, 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]):
+ if any((self.x, self.y, self.rotation, self.scale_x, self.scale_y)):
context.restore()
+ context.new_path() #forget about us
+
def _on_click(self, button_state):
self.emit("on-click", button_state)
if self.parent and isinstance(self.parent, Sprite):
@@ -575,10 +788,100 @@ class Sprite(gtk.Object):
# scene will call us when there is mouse
self.emit("on-mouse-out")
- def _on_drag(self, x, y):
+ def _on_drag_start(self, event):
# scene will call us when there is mouse
- self.emit("on-drag", (x, y))
+ self.emit("on-drag-start", event)
+
+ def _on_drag(self, event):
+ # scene will call us when there is mouse
+ self.emit("on-drag", event)
+
+ def _on_drag_finish(self, event):
+ # scene will call us when there is mouse
+ self.emit("on-drag-finish", event)
+
+class BitmapSprite(Sprite):
+ """Caches given image data in a surface similar to targets, which ensures
+ that drawing it will be quick and low on CPU.
+ Image data can be either :class:`cairo.ImageSurface` or :class:`gtk.gdk.Pixbuf`
+ """
+ def __init__(self, image_data = None, **kwargs):
+ Sprite.__init__(self, **kwargs)
+
+ #: image data
+ self.image_data = image_data
+
+ self._surface = None
+
+ def __setattr__(self, name, val):
+ Sprite.__setattr__(self, name, val)
+ if name == 'image_data':
+ self.__dict__['_surface'] = None
+ if self.image_data:
+ self.__dict__['width'] = self.image_data.get_width()
+ self.__dict__['height'] = self.image_data.get_height()
+
+ def _draw(self, context, opacity = 1):
+ if self.image_data is None or self.width is None or self.height is None:
+ return
+ if not self._surface:
+ # caching image on surface similar to the target
+ self._surface = context.get_target().create_similar(cairo.CONTENT_COLOR_ALPHA,
+ self.width,
+ self.height)
+
+
+ local_context = gtk.gdk.CairoContext(cairo.Context(self._surface))
+ if isinstance(self.image_data, gtk.gdk.Pixbuf):
+ local_context.set_source_pixbuf(self.image_data, 0, 0)
+ else:
+ local_context.set_source_surface(self.image_data)
+ local_context.paint()
+
+ # add instructions with the resulting surface
+ self.graphics.set_source_surface(self._surface)
+ self.graphics.paint()
+ self.graphics.rectangle(0, 0, self.width, self.height)
+
+
+ Sprite._draw(self, context, opacity)
+
+
+class Image(BitmapSprite):
+ """Displays image by path"""
+ def __init__(self, path, **kwargs):
+ BitmapSprite.__init__(self, **kwargs)
+
+ #: path to the image
+ self.path = path
+
+ def __setattr__(self, name, val):
+ BitmapSprite.__setattr__(self, name, val)
+ if name == 'path': # load when the value is set to avoid penalty on render
+ self.image_data = cairo.ImageSurface.create_from_png(self.path)
+
+
+
+class Icon(BitmapSprite):
+ """Displays icon by name and size in the theme"""
+ def __init__(self, name, size=24, **kwargs):
+ BitmapSprite.__init__(self, **kwargs)
+ self.theme = gtk.icon_theme_get_default()
+
+ #: icon name from theme
+ self.name = name
+
+ #: icon size in pixels
+ self.size = size
+
+ def __setattr__(self, name, val):
+ BitmapSprite.__setattr__(self, name, val)
+ if name in ('name', 'size'): # no other reason to discard cache than just on path change
+ if self.__dict__.get('name') and self.__dict__.get('size'):
+ self.image_data = self.theme.load_icon(self.name, self.size, 0)
+ else:
+ self.image_data = None
class Label(Sprite):
@@ -613,36 +916,36 @@ class Label(Sprite):
#: font size
self.size = size
+ self.__surface = None
+
self.connect("on-render", self.on_render)
def __setattr__(self, name, val):
- Sprite.__setattr__(self, name, val)
+ if self.__dict__.get(name, "hamster_graphics_no_value_really") != val:
+ if name == "width" and val and self.__dict__.get('_bounds_width') and val * pango.SCALE == self.__dict__['_bounds_width']:
+ return
- if name == "width":
- # setting width means consumer wants to contrain the label
- if val is None or val == -1:
- self.__dict__['_bounds_width'] = -1
- else:
- self.__dict__['_bounds_width'] = val * pango.SCALE
+ Sprite.__setattr__(self, name, val)
- 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__:
- self._set_dimensions()
+ if name == "width":
+ # setting width means consumer wants to contrain the label
+ if val is None or val == -1:
+ self.__dict__['_bounds_width'] = -1
+ else:
+ 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__:
+ self._set_dimensions()
def on_render(self, sprite):
- self.graphics.clear()
if not self.text:
return
- if self.interactive: #if label is interactive, draw invisible bounding box for simple hit calculations
- self.graphics.set_color("#000", 0)
- self.graphics.rectangle(0,0, self.width, self.height)
- self.graphics.stroke()
-
self.graphics.set_color(self.color)
self.graphics.show_layout(self.text, self.font_desc,
self.alignment,
@@ -650,6 +953,12 @@ class Label(Sprite):
self.wrap,
self.ellipsize)
+ if self._bounds_width != -1:
+ rect_width = self._bounds_width / pango.SCALE
+ else:
+ rect_width = self.width
+ self.graphics.rectangle(0, 0, rect_width, self.height)
+
def _set_dimensions(self):
context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)))
@@ -694,7 +1003,6 @@ class Rectangle(Sprite):
self.connect("on-render", self.on_render)
def on_render(self, sprite):
- self.graphics.clear()
self.graphics.rectangle(0, 0, self.width, self.height, self.corner_radius)
self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
@@ -719,12 +1027,10 @@ class Polygon(Sprite):
self.connect("on-render", self.on_render)
def on_render(self, sprite):
- self.graphics.clear()
if not self.points: return
self.graphics.move_to(*self.points[0])
- for point in self.points:
- self.graphics.line_to(*point)
+ self.graphics.line_to(self.points)
self.graphics.close_path()
self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
@@ -752,7 +1058,6 @@ class Circle(Sprite):
self.connect("on-render", self.on_render)
def on_render(self, sprite):
- self.graphics.clear()
if self.width == self.height:
self.graphics.circle(self.width, self.width / 2.0, self.width / 2.0)
else:
@@ -760,26 +1065,31 @@ class Circle(Sprite):
self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
class Scene(gtk.DrawingArea):
- """ Widget for displaying sprites.
+ """ Drawing area for displaying sprites.
Add sprites to the Scene by calling :func:`add_child`.
Scene is descendant of `gtk.DrawingArea <http://www.pygtk.org/docs/pygtk/class-gtkdrawingarea.html>`_
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-drag-start": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+ "on-drag-finish": (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-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"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,)),
}
@@ -805,9 +1115,13 @@ class Scene(gtk.DrawingArea):
#: 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
+ #: Scene width. Will be `None` until first expose (that is until first
+ #: on-enter-frame signal below).
+ self.width = None
+
+ #: Scene height. Will be `None` until first expose (that is until first
+ #: on-enter-frame signal below).
+ self.height = None
#: instance of :class:`pytweener.Tweener` that is used by
#: :func:`animate` function, but can be also accessed directly for advanced control.
@@ -818,12 +1132,15 @@ class Scene(gtk.DrawingArea):
#: 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
+ #: Last known x position of the mouse (set on expose event)
+ self.mouse_x = None
+
+ #: Last known y position of the mouse (set on expose event)
+ self.mouse_y = None
+
#: Mouse cursor appearance.
#: Replace with your own cursor or set to False to have no cursor.
#: None will revert back the default behavior
@@ -833,29 +1150,42 @@ class Scene(gtk.DrawingArea):
self._blank_cursor = gtk.gdk.Cursor(blank_pixmap, blank_pixmap, gtk.gdk.Color(), gtk.gdk.Color(), 0, 0)
+ #: Miminum distance in pixels for a drag to occur
+ self.drag_distance = 1
+
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.__drag_started = False
+ self.__drag_start_x, self.__drag_start_y = None, None
self._mouse_in = False
self.__drawing_queued = False
- self.__drag_x, self.__drag_y = None, None
self.__last_expose_time = dt.datetime.now()
+ self.__last_cursor = None
+
+ self._redraw_in_progress = False
def add_child(self, *sprites):
"""Add one or several :class:`graphics.Sprite` sprites to scene """
for sprite in sprites:
+ if sprite.parent:
+ sprite.parent.remove_child(sprite)
self.sprites.append(sprite)
-
+ sprite.parent = self
self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
+ def remove_child(self, *sprites):
+ """Remove one or several :class:`graphics.Sprite` sprites from scene """
+ for sprite in sprites:
+ self.sprites.remove(sprite)
+ sprite.parent = None
+
def clear(self):
"""Remove all sprites from scene"""
- self.sprites = []
+ self.remove_child(*self.sprites)
+
def redraw(self):
"""Queue redraw. The redraw will be performed not more often than
@@ -918,17 +1248,20 @@ class Scene(gtk.DrawingArea):
self.fps = 1 / ((now - self.__last_expose_time).microseconds / 1000000.0)
self.__last_expose_time = now
+ self.mouse_x, self.mouse_y, mods = self.get_window().get_pointer()
+
+ self._redraw_in_progress = True
self.emit("on-enter-frame", context)
for sprite in self.sprites:
sprite._draw(context)
- self._check_mouse(self.mouse_x, self.mouse_y)
+ self.__check_mouse(self.mouse_x, self.mouse_y)
self.emit("on-finish-frame", context)
+ self._redraw_in_progress = False
- """ mouse events """
def all_sprites(self, sprites = None):
- """returns flat list of the sprite tree for simplified iteration"""
+ """Returns flat list of the sprite tree for simplified iteration"""
if sprites is None:
sprites = self.sprites
@@ -939,78 +1272,55 @@ class Scene(gtk.DrawingArea):
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
- matrix.rotate(self._drag_sprite.parent.rotation)
- matrix.invert()
+ def all_visible_sprites(self, sprites = None):
+ """Returns flat list of just the visible sprites - avoid children whos
+ parents are not displayed"""
+ if sprites is None:
+ sprites = self.sprites
- if not self.__drag_x:
- x1,y1 = matrix.transform_point(self._mouse_drag[0],
- self._mouse_drag[1])
+ for sprite in sprites:
+ if sprite.visible:
+ yield sprite
+ if sprite.sprites:
+ for child in self.all_visible_sprites(sprite.sprites):
+ yield child
- 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
+ def get_sprite_at_position(self, x, y):
+ """Returns the topmost visible interactive sprite for given coordinates"""
+ over = None
+ for sprite in self.all_visible_sprites():
+ if (sprite.interactive or sprite.draggable) and self.__check_hit(sprite, x, y):
+ over = sprite
+ return over
- 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))
- self.redraw()
- return
- else:
- # avoid double mouse checks - the redraw will also check for mouse!
- if not self.__drawing_queued:
- self._check_mouse(event.x, event.y)
+ def __check_hit(self, sprite, x, y):
+ if sprite == self._drag_sprite:
+ return True
- self.emit("on-mouse-move", event)
+ return sprite.check_hit(x, y)
- def _check_mouse(self, mouse_x, mouse_y):
- if mouse_x is None or self._mouse_in == False:
+ def __check_mouse(self, x, y):
+ if x is None or self._mouse_in == False:
return
- custom_mouse = self.mouse_cursor is not None
+ cursor = gtk.gdk.ARROW # default
- cursor = gtk.gdk.ARROW
- if custom_mouse:
- if self.mouse_cursor == False:
- cursor = self._blank_cursor
- else:
- cursor = self.mouse_cursor
+ if self.mouse_cursor is not None:
+ 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
-
+ over = self.get_sprite_at_position(x, y)
if over:
- if custom_mouse == False:
+ if over.mouse_cursor is not None:
+ cursor = over.mouse_cursor
+
+ elif self.mouse_cursor is None:
+ # resort to defaults
if over.draggable:
cursor = gtk.gdk.FLEUR
else:
@@ -1020,21 +1330,79 @@ class Scene(gtk.DrawingArea):
over._on_mouse_over()
self.emit("on-mouse-over", over)
- self.redraw()
if self._mouse_sprite and self._mouse_sprite != over:
self._mouse_sprite._on_mouse_out()
self.emit("on-mouse-out", self._mouse_sprite)
- self.redraw()
+ self._mouse_sprite = over
+ if cursor == False:
+ cursor = self._blank_cursor
+
+ if not self.__last_cursor or cursor != self.__last_cursor:
+ if isinstance(cursor, gtk.gdk.Cursor):
+ self.window.set_cursor(cursor)
+ else:
+ self.window.set_cursor(gtk.gdk.Cursor(cursor))
+
+ self.__last_cursor = cursor
+
+
+ """ mouse events """
+ def __on_mouse_move(self, area, event):
+ state = event.state
+
+
+ if self._drag_sprite and self._drag_sprite.draggable \
+ and gtk.gdk.BUTTON1_MASK & event.state:
+ # dragging around
+ drag_started = (self.__drag_start_x is not None and \
+ (self.__drag_start_x - event.x) ** 2 + \
+ (self.__drag_start_y - event.y) ** 2 > self.drag_distance ** 2)
+
+ if drag_started and not self.__drag_started:
+ matrix = cairo.Matrix()
+ if self._drag_sprite.parent and isinstance(self._drag_sprite.parent, Sprite):
+ # TODO - this currently works only until second level
+ # should take all parents into account
+ matrix.rotate(self._drag_sprite.parent.rotation)
+ matrix.invert()
+
+ x1,y1 = matrix.transform_point(self.__drag_start_x,
+ self.__drag_start_y)
+ self._drag_sprite.drag_x = x1 - self._drag_sprite.x
+ self._drag_sprite.drag_y = y1 - self._drag_sprite.y
+
+ self._drag_sprite._on_drag_start(event)
+ self.emit("on-drag-start", self._drag_sprite, event)
+
+
+ self.__drag_started = self.__drag_started or drag_started
+
+ if self.__drag_started:
+ matrix = cairo.Matrix()
+ if self._drag_sprite.parent and isinstance(self._drag_sprite.parent, Sprite):
+ # TODO - this currently works only until second level
+ # should take all parents into account
+ matrix.rotate(self._drag_sprite.parent.rotation)
+ matrix.invert()
+
+ mouse_x, mouse_y = matrix.transform_point(event.x, event.y)
+ new_x = mouse_x - self._drag_sprite.drag_x
+ new_y = mouse_y - self._drag_sprite.drag_y
- self._mouse_sprite = over
- if isinstance(cursor, gtk.gdk.Cursor):
- self.window.set_cursor(cursor)
+ self._drag_sprite.x, self._drag_sprite.y = new_x, new_y
+ self._drag_sprite._on_drag(event)
+ self.emit("on-drag", self._drag_sprite, event)
+
+ return
else:
- self.window.set_cursor(gtk.gdk.Cursor(cursor))
+ # 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 __on_mouse_enter(self, area, event):
self._mouse_in = True
@@ -1044,64 +1412,39 @@ class Scene(gtk.DrawingArea):
if self._mouse_sprite:
self.emit("on-mouse-out", self._mouse_sprite)
self._mouse_sprite = None
- self.redraw()
-
- def _check_hit(self, sprite, x, y):
- if sprite == self._drag_sprite:
- return True
-
- if not sprite.graphics.extents:
- return False
-
- sprite_x, sprite_y, sprite_x2, sprite_y2 = sprite.graphics.extents
-
- if sprite_x <= x <= sprite_x2 and sprite_y <= y <= sprite_y2:
- paths = sprite.graphics.paths
- if not paths:
- return True
-
- context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, self.width, self.height))
- for path in paths:
- context.append_path(path)
- return context.in_fill(x, y)
- else:
- return False
def __on_button_press(self, area, event):
- x = event.x
- y = event.y
- state = event.state
- self._mouse_drag = (x, y)
+ self.__drag_start_x, self.__drag_start_y = event.x, event.y
+
+ self._drag_sprite = self.get_sprite_at_position(event.x, event.y)
+ if self._drag_sprite and self._drag_sprite.draggable == False:
+ self._drag_sprite = None
- 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 = dt.datetime.now()
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 (dt.datetime.now() - 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
+ # trying to not emit click and drag-finish at the same time
+ click = not self.__drag_started or (event.x - self.__drag_start_x) ** 2 + \
+ (event.y - self.__drag_start_y) ** 2 < self.drag_distance
+ if (click and self.__drag_started == False) or not self._drag_sprite:
+ target = self.get_sprite_at_position(event.x, event.y)
if target:
target._on_click(event.state)
self.emit("on-click", event, target)
- self.emit("on-mouse-up")
+ if self._drag_sprite:
+ self._drag_sprite._on_drag_finish(event)
+ self.emit("on-drag-finish", self._drag_sprite, event)
+
+ self._drag_sprite.drag_x, self._drag_sprite.drag_y = None, None
+ self._drag_sprite = None
+
+ self.__drag_started = False
+ self.__drag_start_x, self__drag_start_y = None, None
+ self.emit("on-mouse-up", event)
+
+ def __on_scroll(self, area, event):
+ self.emit("on-scroll", event)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]