[hamster-applet] performance updates and a fix to the strangely leaking gtk.gdk.CairoContext.create_layout (just cach



commit 3cecb353e015c4f1b6f010d00b8adfe4186da6b3
Author: Toms Bauģis <toms baugis gmail com>
Date:   Fri Dec 3 15:24:12 2010 +0000

    performance updates and a fix to the strangely leaking gtk.gdk.CairoContext.create_layout (just caching the layout for now; makes sense too)

 src/hamster/utils/graphics.py |  944 +++++++++++++++++++++++++++--------------
 1 files changed, 631 insertions(+), 313 deletions(-)
---
diff --git a/src/hamster/utils/graphics.py b/src/hamster/utils/graphics.py
index d9e3054..e6ca0cf 100644
--- a/src/hamster/utils/graphics.py
+++ b/src/hamster/utils/graphics.py
@@ -19,6 +19,16 @@ except: # we can also live without tweener. Scene.animate will not work
 import colorsys
 from collections import deque
 
+if cairo.version in ('1.8.2', '1.8.4'):
+    # in these two cairo versions the matrix multiplication was flipped
+    # http://bugs.freedesktop.org/show_bug.cgi?id=19221
+    def cairo_matrix_multiply(matrix1, matrix2):
+        return matrix2 * matrix1
+else:
+    def cairo_matrix_multiply(matrix1, matrix2):
+        return matrix1 * matrix2
+
+
 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])")
@@ -80,6 +90,7 @@ class Colors(object):
 
 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
@@ -98,14 +109,15 @@ class Graphics(object):
         self.extents = None     # bounds of the object, only if interactive
         self.paths = None       # paths for mouse hit checks
         self._last_matrix = None
-        self.__new_instructions = deque() # instruction set until it is converted into path-based instructions
-        self.__instruction_cache = None
+        self.__new_instructions = [] # instruction set until it is converted into path-based instructions
+        self.__instruction_cache = []
         self.cache_surface = None
+        self._cache_layout = None
 
     def clear(self):
         """clear all instructions"""
-        self.__new_instructions = deque()
-        self.__instruction_cache = None
+        self.__new_instructions = []
+        self.__instruction_cache = []
         self.paths = []
 
     @staticmethod
@@ -121,6 +133,11 @@ class Graphics(object):
         self._add_instruction(self._fill,)
 
     @staticmethod
+    def _mask(context, pattern): context.mask(pattern)
+    def mask(self, pattern):
+        self._add_instruction(self._mask, pattern)
+
+    @staticmethod
     def _stroke_preserve(context): context.stroke_preserve()
     def stroke_preserve(self, color = None, alpha = 1):
         if color or alpha < 1:self.set_color(color, alpha)
@@ -143,6 +160,16 @@ class Graphics(object):
         self._add_instruction(self._paint,)
 
     @staticmethod
+    def _set_font_face(context, face): context.set_font_face(face)
+    def set_font_face(self, face):
+        self._add_instruction(self._set_font_face, face)
+
+    @staticmethod
+    def _set_font_size(context, size): context.set_font_size(size)
+    def set_font_size(self, size):
+        self._add_instruction(self._set_font_size, size)
+
+    @staticmethod
     def _set_source(context, image):
         context.set_source(image)
     def set_source(self, image, x = 0, y = 0):
@@ -172,6 +199,11 @@ class Graphics(object):
 
 
     @staticmethod
+    def _clip(context): context.clip()
+    def clip(self):
+        self._add_instruction(self._clip)
+
+    @staticmethod
     def _translate(context, x, y): context.translate(x, y)
     def translate(self, x, y):
         self._add_instruction(self._translate, x, y)
@@ -340,11 +372,10 @@ class Graphics(object):
 
 
     @staticmethod
-    def _show_layout(context, text, font_desc, alignment, width, wrap, ellipsize):
-        layout = context.create_layout()
+    def _show_layout(context, layout, text, font_desc, alignment, width, wrap, ellipsize):
         layout.set_font_description(font_desc)
         layout.set_markup(text)
-        layout.set_width(width)
+        layout.set_width(width or -1)
         layout.set_alignment(alignment)
 
         if width > 0:
@@ -372,39 +403,33 @@ class Graphics(object):
         return layout
 
 
-    def show_text(self, text, size = None, color = None):
+    def show_label(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)
 
+
+    @staticmethod
+    def _show_text(context, text): context.show_text(text)
+    def show_text(self, text):
+        self._add_instruction(self._show_text, text)
+
+    @staticmethod
+    def _text_path(context, text): context.text_path(text)
+    def text_path(self, text):
+        """this function is most likely to change"""
+        self._add_instruction(self._text_path, text)
+
     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, instruction):
-        context.save()
-        context.identity_matrix()
-
-        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]),
-                        max(self.extents[2], new_extents[2]),
-                        max(self.extents[3], new_extents[3]))
-
-        self.paths.append(context.copy_path())
-
-        context.restore()
+        layout = self._cache_layout = self._cache_layout or gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0))).create_layout()
 
+        self._add_instruction(self._show_layout, layout, text, font_desc, alignment, width, wrap, ellipsize)
 
     def _add_instruction(self, function, *params):
         if self.context:
@@ -414,102 +439,30 @@ class Graphics(object):
             self.__new_instructions.append((function, params))
 
 
-    def _draw(self, context, opacity, with_extents = False):
+    def _draw(self, context, opacity):
         """draw accumulated instructions in context"""
 
         # 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
-
-        if self.__new_instructions: #new stuff!
-            self.__instruction_cache = deque()
-            current_color = None
-            current_line = None
-            instruction_cache = []
-
-            while self.__new_instructions:
-                instruction, args = self.__new_instructions.popleft()
-
-                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.__instruction_cache.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._new_path, self._stroke, self._fill,
-                                     self._stroke_preserve,
-                                     self._fill_preserve):
-                    self.__instruction_cache.append((context.copy_path(),
-                                                     current_color,
-                                                     current_line,
-                                                     instruction, ()))
-                    instruction_cache = []
-                    if check_extents:
-                        self._remember_path(context, instruction)
-                else:
-                    # the rest are non-special
-                    instruction_cache.append((instruction, args))
-
-                if opacity < 1 and instruction == self._set_color:
-                    self._set_color(context, args[0], args[1], args[2], args[3] * opacity)
-                elif opacity < 1 and instruction == self._paint:
-                    context.paint_with_alpha(opacity)
-                else:
-                    instruction(context, *args) # reset even on preserve as the instruction will preserve it instead
-
-
-            # last one
-            if check_extents and instruction not in (self._fill, self._stroke, self._fill_preserve, self._stroke_preserve):
-                self._remember_path(context, self._fill)
-
-            # also empty the temporary cache that was not met by a stroke at the end
-            while instruction_cache: # stroke is missing so we just cache
-                instruction, args = instruction_cache.pop(0)
-                self.__instruction_cache.append((None, None, None, instruction, args))
-
-
+        fresh_draw = self.__new_instructions and len(self.__new_instructions) > 0
+        if fresh_draw: #new stuff!
+            self.paths = []
+            self.__instruction_cache = self.__new_instructions
+            self.__new_instructions = []
         else:
             if not self.__instruction_cache:
                 return
 
-            for path, color, line, instruction, args in self.__instruction_cache:
-                if color:
-                    if opacity < 1:
-                        self._set_color(context, color[0], color[1], color[2], color[3] * opacity)
-                    else:
-                        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._fill)
+        for instruction, args in self.__instruction_cache:
+            if fresh_draw and instruction in (self._new_path, self._stroke, self._fill, self._clip):
+                self.paths.append(context.copy_path())
 
-                if instruction:
-                    if instruction == self._paint and opacity < 1:
-                        context.paint_with_alpha(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)
+            if opacity < 1 and instruction == self._set_color:
+                self._set_color(context, args[0], args[1], args[2], args[3] * opacity)
+            elif opacity < 1 and instruction == self._paint:
+                context.paint_with_alpha(opacity)
+            else:
+                instruction(context, *args)
 
-        self._last_matrix = context.get_matrix()
 
 
     def _draw_as_bitmap(self, context, opacity):
@@ -518,8 +471,11 @@ class Graphics(object):
             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:
+        matrix_changed = matrix != self._last_matrix
+        new_instructions = len(self.__new_instructions) > 0
+
+        if new_instructions or matrix_changed:
+            if new_instructions:
                 self.__instruction_cache = list(self.__new_instructions)
                 self.__new_instructions = deque()
 
@@ -531,13 +487,24 @@ class Graphics(object):
                 return
 
             # instructions that end path
-            path_end_instructions = (self._new_path, self._stroke, self._fill, self._stroke_preserve, self._fill_preserve)
+            path_end_instructions = (self._new_path, self._clip, self._stroke, self._fill, self._stroke_preserve, self._fill_preserve)
 
-            # measure the path extents so we know the size of surface
+            # measure the path extents so we know the size of cache surface
             # also to save some time use the context to paint for the first time
+            extents = gtk.gdk.Rectangle()
             for instruction, args in self.__instruction_cache:
                 if instruction in path_end_instructions:
-                    self._remember_path(context, instruction)
+                    self.paths.append(context.copy_path())
+
+                    ext = context.path_extents()
+                    ext = gtk.gdk.Rectangle(int(ext[0]), int(ext[1]),
+                                            int(ext[2]-ext[0]), int(ext[3]-ext[1]))
+                    if extents.width and extents.height:
+                        if ext:
+                            extents = extents.union(ext)
+                    else:
+                        extents = ext
+
 
                 if instruction in (self._set_source_pixbuf, self._set_source_surface):
                     # draw a rectangle around the pathless instructions so that the extents are correct
@@ -555,24 +522,45 @@ class Graphics(object):
 
 
             if instruction not in path_end_instructions: # last one
-                self._remember_path(context, self._fill)
+                self.paths.append(context.copy_path())
 
-            # 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])
+                ext = context.path_extents()
+                if any((extents.x, extents.y, extents.width, extents.height)):
+                    if ext:
+                        extents = extents.union(gtk.gdk.Rectangle(int(ext[0]), int(ext[1]),
+                                                                  int(ext[2]-ext[0]), int(ext[3]-ext[1])))
+                else:
+                    extents = ext
 
-            ctx.transform(matrix)
-            for instruction, args in self.__instruction_cache:
-                instruction(ctx, *args)
+
+            # avoid re-caching if we have just moved
+            just_transforms = new_instructions == False and \
+                              matrix and self._last_matrix \
+                              and all([matrix[i] == self._last_matrix[i] for i in range(4)])
+
+            # TODO - this does not look awfully safe
+            extents.x += matrix[4]
+            extents.y += matrix[5]
+            self.extents = extents
+
+            if not just_transforms:
+
+                # now draw the instructions on the caching surface
+                w = int(extents.width) + 1
+                h = int(extents.height) + 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(-extents.x, -extents.y)
+
+                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.translate(self.extents.x, self.extents.y)
             context.set_source_surface(self.cache_surface)
             if opacity < 1:
                 context.paint_with_alpha(opacity)
@@ -600,8 +588,13 @@ class Sprite(gtk.Object):
         "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-render": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
     }
+
+    transformation_flags = set(('x', 'y', 'rotation', 'scale_x', 'scale_y', 'pivot_x', 'pivot_y'))
+    graphics_unrelated_flags = set(('drag_x', 'drag_y', '_matrix', 'sprites', '_stroke_context'))
+    dirty_flags = set(('opacity', 'visible', 'z_order'))
+
     def __init__(self, x = 0, y = 0,
                  opacity = 1, visible = True,
                  rotation = 0, pivot_x = 0, pivot_y = 0,
@@ -659,11 +652,11 @@ class Sprite(gtk.Object):
 
         #: x position of the cursor within mouse upon drag. change this value
         #: in on-drag-start to adjust drag point
-        self.drag_x = None
+        self.drag_x = 0
 
         #: 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.drag_y = 0
 
         #: Whether the sprite should be cached as a bitmap. Default: true
         #: Generally good when you have many static sprites
@@ -675,24 +668,60 @@ class Sprite(gtk.Object):
         self.snap_to_pixel = snap_to_pixel
 
         self.__dict__["_sprite_dirty"] = True # flag that indicates that the graphics object of the sprite should be rendered
+        self.__dict__["_sprite_moved"] = True # flag that indicates that the graphics object of the sprite should be rendered
+
+        self._matrix = None
+        self._prev_parent_matrix = None
+
+        self._extents = None
+        self._prev_extents = None
+
+
+
 
     def __setattr__(self, name, val):
-        if self.__dict__.get(name, "hamster_graphics_no_value_really") == val:
-            return
-        self.__dict__[name] = val
+        try:
+            setter = self.__class__.__dict__[name].__set__
+        except (AttributeError,  KeyError):
+            if self.__dict__.get(name, "hamster_graphics_no_value_really") == val:
+                return
+            self.__dict__[name] = val
 
-        if name not in ('x', 'y', 'rotation', 'scale_x', 'scale_y', 'opacity', 'visible', 'z_order'):
-            self.__dict__["_sprite_dirty"] = True
+            if name == 'parent':
+                self._prev_parent_matrix = None
+                return
+
+            if name == '_prev_parent_matrix':
+                self.__dict__['_extents'] = None
+                for sprite in self.sprites:
+                    sprite._prev_parent_matrix = None
+                return
+
+
+            if name in self.transformation_flags:
+                self.__dict__['_matrix'] = None
+                self.__dict__['_extents'] = None
+                for sprite in self.sprites:
+                    sprite._prev_parent_matrix = None
+
+
+            if name not in (self.transformation_flags ^ self.graphics_unrelated_flags ^ self.dirty_flags):
+                self.__dict__["_sprite_dirty"] = True
+                self.__dict__['_extents'] = None
+
+            if name == 'opacity' and self.__dict__.get("cache_as_bitmap") and self.__dict__.get("graphics"):
+                # invalidating cache for the bitmap version as that paints opacity in the image
+                self.graphics._last_matrix = None
+            elif name == 'interactive' and self.__dict__.get("graphics"):
+                # when suddenly item becomes interactive, it well can be that the extents had not been
+                # calculated
+                self.graphics._last_matrix = None
+            elif name == 'z_order' and self.__dict__.get('parent'):
+                self.parent._sort()
+
+        else:
+            setter(self, val)
 
-        if name == 'opacity' and self.__dict__.get("cache_as_bitmap") and self.__dict__.get("graphics"):
-            # invalidating cache for the bitmap version as that paints opacity in the image
-            self.graphics._last_matrix = None
-        elif name == 'interactive' and self.__dict__.get("graphics"):
-            # when suddenly item becomes interactive, it well can be that the extents had not been
-            # calculated
-            self.graphics._last_matrix = None
-        elif name == 'z_order' and self.__dict__.get('parent'):
-            self.parent._sort()
 
         self.redraw()
 
@@ -703,7 +732,10 @@ class Sprite(gtk.Object):
     def add_child(self, *sprites):
         """Add child sprite. Child will be nested within parent"""
         for sprite in sprites:
+            if sprite == self:
+                raise Exception("trying to add sprite to itself")
             if sprite.parent:
+                sprite.x, sprite.y = self.from_scene_coords(*sprite.to_scene_coords())
                 sprite.parent.remove_child(sprite)
 
             self.sprites.append(sprite)
@@ -716,30 +748,71 @@ class Sprite(gtk.Object):
             self.sprites.remove(sprite)
             sprite.parent = None
 
+    def bring_to_front(self):
+        """adjusts sprite's z-order so that the sprite is on top of it's
+        siblings"""
+        if not self.parent:
+            return
+        self.z_order = self.parent.sprites[-1].z_order + 1
+
+    def send_to_back(self):
+        """adjusts sprite's z-order so that the sprite is behind it's
+        siblings"""
+        if not self.parent:
+            return
+        self.z_order = self.parent.sprites[0].z_order - 1
+
+
+    def get_extents(self):
+        """measure the extents of the sprite's graphics. if context is provided
+           will use that to draw the paths"""
+        if self._extents:
+            return self._extents
+
+        context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0))
+        context.transform(self.get_matrix())
+
+        if not self.graphics.paths:
+            self.graphics._draw(context, 1)
+
+
+        if not self.graphics.paths:
+            return None
+
+
+        for path in self.graphics.paths:
+            context.append_path(path)
+        context.identity_matrix()
+
+        ext = context.path_extents()
+        ext = gtk.gdk.Rectangle(int(ext[0]), int(ext[1]),
+                                int(ext[2] - ext[0]), int(ext[3] - ext[1]))
+
+        self.__dict__['_extents'] = ext
+        self._stroke_context = context
+        return ext
+
+
+
+
     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
+        extents = self.get_extents()
 
-        if sprite_x <= x <= sprite_x2 and sprite_y <= y <= sprite_y2:
-            paths = self.graphics.paths
-            if not paths:
-                return True
+        if not extents:
+            return False
 
-            context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0))
-            for path in paths:
-                context.append_path(path)
-            return context.in_fill(x, y)
+        if extents.x <= x <= extents.x + extents.width and extents.y <= y <= extents.y + extents.height:
+            return self._stroke_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):
+            if isinstance(self.parent, Sprite) == False:
                 return self.parent
             else:
                 return self.parent.get_scene()
@@ -768,50 +841,87 @@ class Sprite(gtk.Object):
         scene = self.get_scene()
         if scene:
             scene.animate(self, duration, easing, on_complete, on_update, **kwargs)
+        else:
+            for key, val in kwargs.items():
+                setattr(self, key, val)
 
-    def _draw(self, context, opacity = 1):
+    def get_local_matrix(self):
+        if not self._matrix:
+            self._matrix = cairo.Matrix()
+
+            if self.snap_to_pixel:
+                self._matrix.translate(int(self.x) + int(self.pivot_x), int(self.y) + int(self.pivot_y))
+            else:
+                self._matrix.translate(self.x + self.pivot_x, self.y + self.pivot_y)
+
+            if self.rotation:
+                self._matrix.rotate(self.rotation)
+
+
+            if self.snap_to_pixel:
+                self._matrix.translate(int(-self.pivot_x), int(-self.pivot_y))
+            else:
+                self._matrix.translate(-self.pivot_x, -self.pivot_y)
+
+
+            if self.scale_x != 1 or self.scale_y != 1:
+                self._matrix.scale(self.scale_x, self.scale_y)
+
+        return cairo.Matrix() * self._matrix
+
+
+    def get_matrix(self):
+        """return sprite's current transformation matrix"""
+        if self.parent:
+            return cairo_matrix_multiply(self.get_local_matrix(),
+                                         (self._prev_parent_matrix or self.parent.get_matrix()))
+        else:
+            return self.get_local_matrix()
+
+
+    def from_scene_coords(self, x=0, y=0):
+        """Converts x, y given in the scene coordinates to sprite's local ones
+        coordinates"""
+        matrix = self.get_matrix()
+        matrix.invert()
+        return matrix.transform_point(x, y)
+
+    def to_scene_coords(self, x=0, y=0):
+        """Converts x, y from sprite's local coordinates to scene coordinates"""
+        return self.get_matrix().transform_point(x, y)
+
+    def _draw(self, context, opacity = 1, parent_matrix = None):
         if self.visible is False:
             return
 
-        context.new_path()
-
         if (self._sprite_dirty): # send signal to redo the drawing when sprite is dirty
+            self.__dict__['_extents'] = None
             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 any((self.x, self.y, self.pivot_x, self.pivot_y)):
-                if self.snap_to_pixel:
-                    context.translate(int(self.x) + int(self.pivot_x), int(self.y) + int(self.pivot_y))
-                else:
-                    context.translate(self.x + self.pivot_x, self.y + self.pivot_y)
+        parent_matrix = parent_matrix or cairo.Matrix()
 
-            if self.rotation:
-                context.rotate(self.rotation)
+        # cache parent matrix
+        self._prev_parent_matrix = parent_matrix
 
-            if self.pivot_x or self.pivot_y:
-                if self.snap_to_pixel:
-                    context.translate(int(-self.pivot_x), int(-self.pivot_y))
-                else:
-                    context.translate(-self.pivot_x, -self.pivot_y)
+        matrix = self.get_local_matrix()
+        context.save()
+        context.transform(matrix)
 
-            if self.scale_x != 1 or self.scale_y != 1:
-                context.scale(self.scale_x, self.scale_y)
 
         if self.cache_as_bitmap:
             self.graphics._draw_as_bitmap(context, self.opacity * opacity)
         else:
-            self.graphics._draw(context, self.opacity * opacity, self.interactive or self.draggable)
+            self.graphics._draw(context, self.opacity * opacity)
+
+        self.__dict__['_prev_extents'] = self._extents or self.get_extents()
 
         for sprite in self.sprites:
-            sprite._draw(context, self.opacity * opacity)
+            sprite._draw(context, self.opacity * opacity, cairo_matrix_multiply(matrix, parent_matrix))
 
-        if any((self.x, self.y, self.rotation, self.scale_x, self.scale_y)):
-            context.restore()
 
+        context.restore()
         context.new_path() #forget about us
 
 
@@ -823,31 +933,41 @@ class BitmapSprite(Sprite):
     def __init__(self, image_data = None, **kwargs):
         Sprite.__init__(self, **kwargs)
 
+        self._image_width, self._image_height = None, None
         #: image data
         self.image_data = image_data
 
         self._surface = None
 
+    @property
+    def height(self):
+        return self._image_height
+
+    @property
+    def width(self):
+        return self._image_width
+
+
     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()
+                self.__dict__['_image_width'] = self.image_data.get_width()
+                self.__dict__['_image_height'] = self.image_data.get_height()
 
-    def _draw(self, context, opacity = 1):
+    def _draw(self, context, opacity = 1, parent_matrix = None):
         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,
+            surface = context.get_target().create_similar(cairo.CONTENT_COLOR_ALPHA,
                                                                self.width,
                                                                self.height)
 
 
-            local_context = gtk.gdk.CairoContext(cairo.Context(self._surface))
+            local_context = gtk.gdk.CairoContext(cairo.Context(surface))
             if isinstance(self.image_data, gtk.gdk.Pixbuf):
                 local_context.set_source_pixbuf(self.image_data, 0, 0)
             else:
@@ -855,12 +975,15 @@ class BitmapSprite(Sprite):
             local_context.paint()
 
             # add instructions with the resulting surface
-            self.graphics.set_source_surface(self._surface)
-            self.graphics.paint()
+            self.graphics.clear()
             self.graphics.rectangle(0, 0, self.width, self.height)
+            self.graphics.clip()
+            self.graphics.set_source_surface(surface)
+            self.graphics.paint()
+            self._surface = surface
 
 
-        Sprite._draw(self,  context, opacity)
+        Sprite._draw(self,  context, opacity, parent_matrix)
 
 
 class Image(BitmapSprite):
@@ -900,11 +1023,22 @@ class Icon(BitmapSprite):
 
 
 class Label(Sprite):
+    __gsignals__ = {
+        "on-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
     def __init__(self, text = "", size = 10, color = None,
-                 alignment = pango.ALIGN_LEFT, **kwargs):
+                 alignment = pango.ALIGN_LEFT, font_face = None,
+                 max_width = None, wrap = None, ellipsize = None,
+                 outline_color = None, outline_width = 5,
+                 **kwargs):
         Sprite.__init__(self, **kwargs)
         self.width, self.height = None, None
 
+
+        self._test_context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A8, 0, 0)))
+        self._test_layout = self._test_context.create_layout()
+
+
         #: 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)
@@ -912,30 +1046,51 @@ class Label(Sprite):
         #: color of label either as hex string or an (r,g,b) tuple
         self.color = color
 
-        self._bounds_width = -1
+        #: color for text outline (currently works only with a custom font face)
+        self.outline_color = outline_color
+
+        #: text outline thickness (currently works only with a custom font face)
+        self.outline_width = outline_width
+
+        self._bounds_width = None
 
         #: wrapping method. Can be set to pango. [WRAP_WORD, WRAP_CHAR,
         #: WRAP_WORD_CHAR]
-        self.wrap = None
+        self.wrap = wrap
 
         #: Ellipsize mode. Can be set to pango. [ELLIPSIZE_NONE,
         #: ELLIPSIZE_START, ELLIPSIZE_MIDDLE, ELLIPSIZE_END]
-        self.ellipsize = None
+        self.ellipsize = ellipsize
 
         #: alignment. one of pango.[ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER]
         self.alignment = alignment
 
-        #: label text
-        self.text = text
+        #: label's `FontFace <http://www.cairographics.org/documentation/pycairo/2/reference/text.html#cairo.FontFace>`_
+        self.font_face = font_face
 
         #: font size
         self.size = size
 
+
+        #: maximum  width of the label in pixels. if specified, the label
+        #: will be wrapped or ellipsized depending on the wrap and ellpisize settings
+        self.max_width = max_width
+
+        self._ascent = None # used to determine Y position for when we have a font face
+
         self.__surface = None
 
+        #: label text
+        self.text = text
+
+        self._letter_sizes = {}
+        self._measures = {}
+
 
         self.connect("on-render", self.on_render)
 
+        self.graphics_unrelated_flags = self.graphics_unrelated_flags ^ set(("_letter_sizes", "__surface", "_ascent", "_bounds_width", "_measures"))
+
 
     def __setattr__(self, name, val):
         if self.__dict__.get(name, "hamster_graphics_no_value_really") != val:
@@ -948,14 +1103,172 @@ class Label(Sprite):
             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'] = None
                 else:
                     self.__dict__['_bounds_width'] = val * pango.SCALE
 
-            if name in ("width", "text", "size", "font_desc", "wrap", "ellipsize"):
+            if name in ("width", "text", "size", "font_desc", "wrap", "ellipsize", "max_width"):
+                self._measures = {}
                 # avoid chicken and egg
-                if "text" in self.__dict__ and "size" in self.__dict__:
-                    self._set_dimensions()
+                if hasattr(self, "text") and hasattr(self, "size") and hasattr(self, "font_face"):
+                    self.__dict__['width'], self.__dict__['height'], self.__dict__['_ascent'] = self.measure(self.text)
+
+            if name in("font_desc", "size"):
+                self._letter_sizes = {}
+
+            if name == 'text':
+                self.emit('on-change')
+
+
+    def _wrap(self, text):
+        """wrapping text ourselves when we can't use pango"""
+        if not text:
+            return [], 0
+
+        context = self._test_context
+        context.set_font_face(self.font_face)
+        context.set_font_size(self.size)
+
+
+        if (not self._bounds_width and not self.max_width) or self.wrap is None:
+            return [(text, context.text_extents(text)[4])], context.font_extents()[2]
+
+
+        width = self.max_width or self.width
+
+        letters = {}
+        # measure individual letters
+        if self.wrap in (pango.WRAP_CHAR, pango.WRAP_WORD_CHAR):
+            letters = set(unicode(text))
+            sizes = [self._letter_sizes.setdefault(letter, context.text_extents(letter)[4]) for letter in letters]
+            letters = dict(zip(letters, sizes))
+
+
+        line = ""
+        lines = []
+        running_width = 0
+
+        if self.wrap in (pango.WRAP_WORD, pango.WRAP_WORD_CHAR):
+            # if we wrap by word then we split the whole thing in words
+            # and stick together while they fit. in case if the word does not
+            # fit at all, we break it in pieces
+            while text:
+                fragment, fragment_length = "", 0
+
+                word = re.search("\s", text)
+                if word:
+                    fragment = text[:word.start()+1]
+                else:
+                    fragment = text
+
+                fragment_length = context.text_extents(fragment)[4]
+
+
+                if (fragment_length > width) and self.wrap == pango.WRAP_WORD_CHAR:
+                    # too big to fit in any way
+                    # split in pieces so that we fit in current row as much
+                    # as we can and trust the task of putting things in next row
+                    # to the next run
+                    while fragment and running_width + fragment_length > width:
+                        fragment_length -= letters[fragment[-1]]
+                        fragment = fragment[:-1]
+
+                    lines.append((line + fragment, running_width + fragment_length))
+                    running_width = 0
+                    fragment_length = 0
+                    line = ""
+
+
+
+                else:
+                    # otherwise the usual squishing
+                    if running_width + fragment_length <= width:
+                        line += fragment
+                    else:
+                        lines.append((line, running_width))
+                        running_width = 0
+                        line = fragment
+
+
+
+                running_width += fragment_length
+                text = text[len(fragment):]
+
+        elif self.wrap == pango.WRAP_CHAR:
+            # brute force glueing while we have space
+            for fragment in text:
+                fragment_length = letters[fragment]
+
+                if running_width + fragment_length <= width:
+                    line += fragment
+                else:
+                    lines.append((line, running_width))
+                    running_width = 0
+                    line = fragment
+
+                running_width += fragment_length
+
+        if line:
+            lines.append((line, running_width))
+
+        return lines, context.font_extents()[2]
+
+
+
+
+    def measure(self, text):
+        """measures given text with label's font and size.
+        returns width, height and ascent. Ascent's null in case if the label
+        does not have font face specified (and is thusly using pango)"""
+
+        if text in self._measures:
+            return self._measures[text]
+
+        width, height, ascent = None, None, None
+
+        context = self._test_context
+        if self.font_face:
+            context.set_font_face(self.font_face)
+            context.set_font_size(self.size)
+            font_ascent, font_descent, font_height = context.font_extents()[:3]
+
+            if self._bounds_width or self.max_width:
+                lines, line_height = self._wrap(text)
+
+                if self._bounds_width:
+                    width = self._bounds_width / pango.SCALE
+                else:
+                    max_width = 0
+                    for line, line_width in lines:
+                        max_width = max(max_width, line_width)
+                    width = max_width
+
+                height = len(lines) * line_height
+                ascent = font_ascent
+            else:
+                width = context.text_extents(text)[4]
+                ascent, height = font_ascent, font_ascent + font_descent
+
+        else:
+            layout = self._test_layout
+            layout.set_font_description(self.font_desc)
+            layout.set_markup(text)
+            layout.set_width((self._bounds_width or -1))
+            layout.set_ellipsize(pango.ELLIPSIZE_NONE)
+
+
+            if self.wrap is not None:
+                layout.set_wrap(self.wrap)
+            else:
+                layout.set_ellipsize(self.ellipsize or pango.ELLIPSIZE_END)
+
+            width, height = layout.get_pixel_size()
+
+
+        self._measures[text] = width, height, ascent
+
+        return self._measures[text]
+
 
     def on_render(self, sprite):
         if not self.text:
@@ -963,40 +1276,65 @@ class Label(Sprite):
             return
 
         self.graphics.set_color(self.color)
-        self.graphics.show_layout(self.text, self.font_desc,
-                                  self.alignment,
-                                  self._bounds_width,
-                                  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)
 
+        rect_width = self.width
 
-    def _set_dimensions(self):
-        context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)))
-        layout = context.create_layout()
+        if self.font_face:
+            self.graphics.set_font_size(self.size)
+            self.graphics.set_font_face(self.font_face)
+            if self._bounds_width or self.max_width:
+                lines, line_height = self._wrap(self.text)
 
+                x, y = 0.5, int(self._ascent) + 0.5
+                for line, line_width in lines:
+                    if self.alignment == pango.ALIGN_RIGHT:
+                        x = self.width - line_width
+                    elif self.alignment == pango.ALIGN_CENTER:
+                        x = (self.width - line_width) / 2
 
-        layout.set_font_description(self.font_desc)
-        layout.set_markup(self.text)
-        layout.set_width(self._bounds_width)
-        layout.set_ellipsize(pango.ELLIPSIZE_NONE)
+                    if self.outline_color:
+                        self.graphics.save_context()
+                        self.graphics.move_to(x, y)
+                        self.graphics.text_path(line)
+                        self.graphics.set_line_style(width=self.outline_width)
+                        self.graphics.fill_stroke(self.outline_color, self.outline_color)
+                        self.graphics.restore_context()
+
+                    self.graphics.move_to(x, y)
+                    self.graphics.set_color(self.color)
+                    self.graphics.show_text(line)
+
+                    y += line_height
+
+            else:
+                if self.outline_color:
+                    self.graphics.save_context()
+                    self.graphics.move_to(0, self._ascent)
+                    self.graphics.text_path(self.text)
+                    self.graphics.set_line_style(width=self.outline_width)
+                    self.graphics.fill_stroke(self.outline_color, self.outline_color)
+                    self.graphics.restore_context()
+
+                self.graphics.move_to(0, self._ascent)
+                self.graphics.show_text(self.text)
 
-        if self.wrap is not None:
-            layout.set_wrap(self.wrap)
         else:
-            layout.set_ellipsize(self.ellipsize or pango.ELLIPSIZE_END)
+            self.graphics.show_layout(self.text, self.font_desc,
+                                      self.alignment,
+                                      self._bounds_width,
+                                      self.wrap,
+                                      self.ellipsize)
+
+            if self._bounds_width:
+                rect_width = self._bounds_width / pango.SCALE
+
+        self.graphics.rectangle(0, 0, rect_width, self.height)
+        self.graphics.clip()
 
-        # 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):
+    def __init__(self, w, h, corner_radius = 0, fill = None, stroke = None, line_width = 1, **kwargs):
         Sprite.__init__(self, **kwargs)
 
         #: width
@@ -1012,18 +1350,15 @@ class Rectangle(Sprite):
         self.stroke = stroke
 
         #: stroke line width
-        self.line_width = 1
+        self.line_width = line_width
 
         #: 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):
-        x, y = 0, 0
-        if self.snap_to_pixel:
-            x, y = 0.5, 0.5
-
-        self.graphics.rectangle(x, y, self.width, self.height, self.corner_radius)
+        self.graphics.set_line_style(width = self.line_width)
+        self.graphics.rectangle(0, 0, self.width, self.height, self.corner_radius)
         self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
 
 
@@ -1121,7 +1456,8 @@ class Scene(gtk.DrawingArea):
             self.set_events(gtk.gdk.POINTER_MOTION_MASK
                             | gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.ENTER_NOTIFY_MASK
                             | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK
-                            | gtk.gdk.SCROLL_MASK)
+                            | gtk.gdk.SCROLL_MASK
+                            | gtk.gdk.KEY_PRESS_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)
@@ -1181,6 +1517,7 @@ class Scene(gtk.DrawingArea):
         self._last_frame_time = None
         self._mouse_sprite = None
         self._drag_sprite = None
+        self._mouse_down_sprite = None
         self.__drag_started = False
         self.__drag_start_x, self.__drag_start_y = None, None
 
@@ -1188,7 +1525,7 @@ class Scene(gtk.DrawingArea):
         self.__last_cursor = None
 
         self.__drawing_queued = False
-        self._redraw_in_progress = True
+        self._redraw_in_progress = False
 
         #: When specified, upon window resize the content will be scaled
         #: relative to original window size. Defaults to False.
@@ -1205,7 +1542,10 @@ class Scene(gtk.DrawingArea):
     def add_child(self, *sprites):
         """Add one or several :class:`Sprite` objects to the scene"""
         for sprite in sprites:
+            if sprite == self:
+                raise Exception("trying to add sprite to itself")
             if sprite.parent:
+                sprite.x, sprite.y = sprite.to_scene_coords(0, 0)
                 sprite.parent.remove_child(sprite)
             self.sprites.append(sprite)
             sprite.parent = self
@@ -1222,6 +1562,11 @@ class Scene(gtk.DrawingArea):
             self.sprites.remove(sprite)
             sprite.parent = None
 
+    # these two mimic sprite functions so parent check can be avoided
+    def from_scene_coords(self, x, y): return x, y
+    def to_scene_coords(self, x, y): return x, y
+    def get_matrix(self): return cairo.Matrix()
+
     def clear(self):
         """Remove all sprites from scene"""
         self.remove_child(*self.sprites)
@@ -1318,48 +1663,32 @@ class Scene(gtk.DrawingArea):
 
         self.width, self.height = event.width, event.height
 
-    def all_sprites(self, sprites = None):
+    def all_visible_sprites(self):
         """Returns flat list of the sprite tree for simplified iteration"""
+        def all_recursive(sprites):
+            for sprite in sprites:
+                if sprite.visible:
+                    yield sprite
+                    if sprite.sprites:
+                        for child in all_recursive(sprite.sprites):
+                            yield child
 
-        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 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
-
-        for sprite in sprites:
-            if sprite.visible:
-                yield sprite
-                if sprite.sprites:
-                    for child in self.all_visible_sprites(sprite.sprites):
-                        yield child
+        return all_recursive(self.sprites)
 
 
     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):
+            if (sprite.interactive or sprite.draggable) and sprite.check_hit(x, y):
                 over = sprite
 
         return over
 
 
-    def __check_hit(self, sprite, x, y):
-        if sprite == self._drag_sprite:
-            return True
-
-        return sprite.check_hit(x, y)
-
 
     def __check_mouse(self, x, y):
         if x is None or self._mouse_in == False:
@@ -1370,31 +1699,33 @@ class Scene(gtk.DrawingArea):
         if self.mouse_cursor is not None:
             cursor = self.mouse_cursor
 
+        if self._drag_sprite:
+            cursor = self._drag_sprite.mouse_cursor or self.mouse_cursor or gtk.gdk.FLEUR
+        else:
+            #check if we have a mouse over
+            over = self.get_sprite_at_position(x, y)
+            if self._mouse_sprite and self._mouse_sprite != over:
+                self._mouse_sprite.emit("on-mouse-out")
+                self.emit("on-mouse-out", self._mouse_sprite)
+                self.redraw()
 
-        #check if we have a mouse over
-        over = self.get_sprite_at_position(x, y)
-        if over:
-            if over.mouse_cursor is not None:
-                cursor = over.mouse_cursor
+            if over:
+                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:
-                    cursor = gtk.gdk.HAND2
+                elif self.mouse_cursor is None:
+                    # resort to defaults
+                    if over.draggable:
+                        cursor = gtk.gdk.FLEUR
+                    else:
+                        cursor = gtk.gdk.HAND2
 
-            if over != self._mouse_sprite:
-                over.emit("on-mouse-over")
-                self.emit("on-mouse-over", over)
-                self.redraw()
+                if over != self._mouse_sprite:
+                    over.emit("on-mouse-over")
+                    self.emit("on-mouse-over", over)
+                    self.redraw()
 
-        if self._mouse_sprite and self._mouse_sprite != over:
-            self._mouse_sprite.emit("on-mouse-out")
-            self.emit("on-mouse-out", self._mouse_sprite)
-            self.redraw()
-
-        self._mouse_sprite = over
+            self._mouse_sprite = over
 
         if cursor == False:
             cursor = self._blank_cursor
@@ -1413,7 +1744,7 @@ class Scene(gtk.DrawingArea):
         state = event.state
 
 
-        if self._drag_sprite and self._drag_sprite.draggable \
+        if self._mouse_down_sprite and self._mouse_down_sprite.draggable \
            and gtk.gdk.BUTTON1_MASK & event.state:
             # dragging around
             drag_started = (self.__drag_start_x is not None and \
@@ -1421,17 +1752,9 @@ class Scene(gtk.DrawingArea):
                            (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()
+                self._drag_sprite = self._mouse_down_sprite
 
-                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.drag_x, self._drag_sprite.drag_y = self._drag_sprite.x, self._drag_sprite.y
 
                 self._drag_sprite.emit("on-drag-start", event)
                 self.emit("on-drag-start", self._drag_sprite, event)
@@ -1441,24 +1764,18 @@ class Scene(gtk.DrawingArea):
             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)
+                diff_x, diff_y = event.x - self.__drag_start_x, event.y - self.__drag_start_y
+                if isinstance(self._drag_sprite.parent, Sprite):
+                    matrix = self._drag_sprite.parent.get_matrix()
                     matrix.invert()
+                    diff_x, diff_y = matrix.transform_distance(diff_x, diff_y)
 
-                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._drag_sprite.x, self._drag_sprite.y = self._drag_sprite.drag_x + diff_x, self._drag_sprite.drag_y + diff_y
 
-
-                self._drag_sprite.x, self._drag_sprite.y = new_x, new_y
                 self._drag_sprite.emit("on-drag", event)
                 self.emit("on-drag", self._drag_sprite, event)
                 self.redraw()
 
-                return
         else:
             # avoid double mouse checks - the redraw will also check for mouse!
             if not self.__drawing_queued:
@@ -1481,17 +1798,20 @@ class Scene(gtk.DrawingArea):
         target = self.get_sprite_at_position(event.x, event.y)
         self.__drag_start_x, self.__drag_start_y = event.x, event.y
 
-        self._drag_sprite = target
-        if self._drag_sprite and self._drag_sprite.draggable == False:
-            self._drag_sprite = None
+        self._mouse_down_sprite = target
 
         if target:
             target.emit("on-mouse-down", event)
         self.emit("on-mouse-down", event)
 
     def __on_button_release(self, area, event):
-        # trying to not emit click and drag-finish at the same time
         target = self.get_sprite_at_position(event.x, event.y)
+
+        if target:
+            target.emit("on-mouse-up", event)
+        self.emit("on-mouse-up", event)
+
+        # 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:
@@ -1508,12 +1828,10 @@ class Scene(gtk.DrawingArea):
 
             self._drag_sprite.drag_x, self._drag_sprite.drag_y = None, None
             self._drag_sprite = None
+        self._mouse_down_sprite = None
 
         self.__drag_started = False
         self.__drag_start_x, self__drag_start_y = None, None
-        if target:
-            target.emit("on-mouse-up", event)
-        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]