Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/sugar/tutorius/overlayer.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/sugar/tutorius/overlayer.py')
-rw-r--r--src/sugar/tutorius/overlayer.py328
1 files changed, 328 insertions, 0 deletions
diff --git a/src/sugar/tutorius/overlayer.py b/src/sugar/tutorius/overlayer.py
new file mode 100644
index 0000000..c08ed4c
--- /dev/null
+++ b/src/sugar/tutorius/overlayer.py
@@ -0,0 +1,328 @@
+"""
+This guy manages drawing of overlayed widgets. The class responsible for drawing
+management (Overlayer) and overlayable widgets are defined here.
+"""
+# Copyright (C) 2009, Tutorius.org
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gobject
+import gtk
+import cairo
+import pangocairo
+
+# This is the CanvasDrawable protocol. Any widget wishing to be drawn on the
+# overlay must implement it. See TextBubble for a sample implementation.
+#class CanvasDrawable(object):
+# """Defines the CanvasDrawable protocol"""
+# no_expose = None
+# def draw_with_context(self, context):
+# """
+# Draws the cairo widget with the passed cairo context.
+# This will be called if the widget is child of an overlayer.
+# """
+# pass
+
+class Overlayer(gtk.Layout):
+ """
+ This guy manages drawing of overlayed widgets. Those can be standard GTK
+ widgets or special "cairoDrawable" widgets which support the defined
+ interface (see the put method).
+
+ @param overlayed widget to be overlayed. Will be resized to full size.
+ """
+ def __init__(self, overlayed=None):
+ gtk.Layout.__init__(self)
+
+ self._overlayed = overlayed
+ if overlayed:
+ self.put(overlayed, 0, 0)
+
+ self.__realizer = self.connect("expose-event", self.__init_realized)
+ self.connect("size-allocate", self.__size_allocate)
+ self.show()
+
+ self.__render_handle = None
+
+ def put(self, child, x, y):
+ """
+ Adds a child widget to be overlayed. This can be, overlay widgets or
+ normal GTK widgets (though normal widgets will alwas appear under
+ cairo widgets due to the rendering chain).
+
+ @param child the child to add
+ @param x the horizontal coordinate for positionning
+ @param y the vertical coordinate for positionning
+ """
+ if hasattr(child, "draw_with_context"):
+ # if the widget has the CanvasDrawable protocol, use it.
+ child.no_expose = True
+ gtk.Layout.put(self, child, x, y)
+
+
+ def __init_realized(self, widget, event):
+ """
+ Initializer to set once widget is realized.
+ Since an expose event is signaled only to realized widgets, we set this
+ callback for the first expose run. It should also be called after
+ beign reparented to ensure the window used for drawing is set up.
+ """
+ assert hasattr(self.window, "set_composited"), \
+ "compositing not supported or widget not realized."
+ self.disconnect(self.__realizer)
+ del self.__realizer
+
+ self.parent.set_app_paintable(True)
+
+ # the parent is composited, so we can access gtk's rendered buffer
+ # and overlay over. If we don't composite, we won't be able to read
+ # pixels and background will be black.
+ self.window.set_composited(True)
+ self.__render_handle = self.parent.connect_after("expose-event", \
+ self.__expose_overlay)
+
+ def __expose_overlay(self, widget, event):
+ """expose event handler to draw the thing."""
+ #get our child (in this case, the event box)
+ child = widget.get_child()
+
+ #create a cairo context to draw to the window
+ ctx = widget.window.cairo_create()
+
+ #the source data is the (composited) event box
+ ctx.set_source_pixmap(child.window,
+ child.allocation.x,
+ child.allocation.y)
+
+ #draw no more than our expose event intersects our child
+ region = gtk.gdk.region_rectangle(child.allocation)
+ rect = gtk.gdk.region_rectangle(event.area)
+ region.intersect(rect)
+ ctx.region (region)
+ ctx.clip()
+
+ ctx.set_operator(cairo.OPERATOR_OVER)
+ # has to be blended and a 1.0 alpha would not make it blend
+ ctx.paint_with_alpha(0.99)
+
+ #draw overlay
+ for drawn_child in self.get_children():
+ if hasattr(drawn_child, "draw_with_context"):
+ drawn_child.draw_with_context(ctx)
+
+
+ def __size_allocate(self, widget, allocation):
+ """
+ Set size allocation (actual gtk widget size) and propagate it to
+ overlayed child
+ """
+ self.allocation = allocation
+ # One may wonder why using size_request instead of size_allocate;
+ # Since widget is laid out in a Layout box, the Layout will honor the
+ # requested size. Using size_allocate could make a nasty nested loop in
+ # some cases.
+ self._overlayed.set_size_request(allocation.width, allocation.height)
+
+
+class TextBubble(gtk.Widget):
+ """
+ A CanvasDrawableWidget drawing a round textbox and a tail pointing
+ to a specified widget.
+ """
+ def __init__(self, text, speaker=None, tailpos=None):
+ """
+ Creates a new cairo rendered text bubble.
+
+ @param text the text to render in the bubble
+ @param speaker the widget to compute the tail position from
+ @param tailpos (optional) position relative to the bubble to use as
+ the tail position, if no speaker
+ """
+ gtk.Widget.__init__(self)
+
+ # FIXME: ensure previous call does not interfere with widget stacking,
+ # as using a gtk.Layout and stacking widgets may reveal a screwed up
+ # order with the cairo widget on top.
+ self.__label = None
+ self.__text_dimentions = None
+
+ self.label = text
+ self.speaker = speaker
+ self.tailpos = tailpos
+ self.line_width = 5
+
+ self.__exposer = self.connect("expose-event", self.__on_expose)
+
+ def draw_with_context(self, context):
+ """
+ Draw using the passed cairo context instead of creating a new cairo
+ context. This eases blending between multiple cairo-rendered
+ widgets.
+ """
+ context.translate(self.allocation.x, self.allocation.y)
+ width = self.allocation.width
+ height = self.allocation.height
+ xradius = width/2
+ yradius = height/2
+ width -= self.line_width
+ height -= self.line_width
+
+ # bubble border
+ context.move_to(self.line_width, yradius)
+ context.curve_to(self.line_width, self.line_width,
+ self.line_width, self.line_width, xradius, self.line_width)
+ context.curve_to(width, self.line_width,
+ width, self.line_width, width, yradius)
+ context.curve_to(width, height, width, height, xradius, height)
+ context.curve_to(self.line_width, height,
+ self.line_width, height, self.line_width, yradius)
+ context.set_line_width(self.line_width)
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.stroke()
+
+ # TODO fetch speaker coordinates
+
+ # draw bubble tail
+ if self.tailpos:
+ context.move_to(xradius-40, yradius)
+ context.line_to(self.tailpos[0], self.tailpos[1])
+ context.line_to(xradius+40, yradius)
+ context.set_line_width(self.line_width)
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ context.stroke_preserve()
+ context.set_source_rgb(1.0, 1.0, 0.0)
+ context.fill()
+
+ # bubble painting. Redrawing the inside after the tail will combine
+ # both shapes.
+ # TODO: we could probably generate the shape at initialization to
+ # lighten computations.
+ context.move_to(self.line_width, yradius)
+ context.curve_to(self.line_width, self.line_width,
+ self.line_width, self.line_width, xradius, self.line_width)
+ context.curve_to(width, self.line_width,
+ width, self.line_width, width, yradius)
+ context.curve_to(width, height, width, height, xradius, height)
+ context.curve_to(self.line_width, height,
+ self.line_width, height, self.line_width, yradius)
+ context.set_source_rgb(1.0, 1.0, 0.0)
+ context.fill()
+
+ # text
+ # FIXME create text layout when setting text or in realize method
+ context.set_source_rgb(0.0, 0.0, 0.0)
+ pangoctx = pangocairo.CairoContext(context)
+ text_layout = pangoctx.create_layout()
+ text_layout.set_text(self.__label)
+ pangoctx.move_to(
+ int((self.allocation.width-self.__text_dimentions[0])/2),
+ int((self.allocation.height-self.__text_dimentions[1])/2))
+ pangoctx.show_layout(text_layout)
+
+ # work done. Be kind to next cairo widgets and reset matrix.
+ context.identity_matrix()
+
+ def do_realize(self):
+ """ Setup gdk window creation. """
+ self.set_flags(gtk.REALIZED | gtk.NO_WINDOW)
+ # TODO: cleanup window creation code as lot here probably isn't
+ # necessary.
+ # See http://www.learningpython.com/2006/07/25/writing-a-custom-widget-using-pygtk/
+ # as the following was taken there.
+ self.window = self.get_parent_window()
+ if not isinstance(self.parent, Overlayer):
+ self.unset_flags(gtk.NO_WINDOW)
+ self.window = gtk.gdk.Window(
+ self.get_parent_window(),
+ width=self.allocation.width,
+ height=self.allocation.height,
+ window_type=gtk.gdk.WINDOW_CHILD,
+ wclass=gtk.gdk.INPUT_OUTPUT,
+ event_mask=self.get_events()|gtk.gdk.EXPOSURE_MASK)
+
+ # Associate the gdk.Window with ourselves, Gtk+ needs a reference
+ # between the widget and the gdk window
+ self.window.set_user_data(self)
+
+ # Attach the style to the gdk.Window, a style contains colors and
+ # GC contextes used for drawing
+ self.style.attach(self.window)
+
+ # The default color of the background should be what
+ # the style (theme engine) tells us.
+ self.style.set_background(self.window, gtk.STATE_NORMAL)
+ self.window.move_resize(*self.allocation)
+
+ def __on_expose(self, widget, event):
+ """Redraw event callback."""
+ ctx = self.window.cairo_create()
+
+ self.draw_with_context(ctx)
+
+ return True
+
+ def _set_label(self, value):
+ """Sets the label and flags the widget to be redrawn."""
+ self.__label = value
+ # FIXME hack to calculate size. necessary because may not have been
+ # realized. We create a fake surface to use builtin math. This should
+ # probably be done at realization and/or on text setter.
+ surf = cairo.SVGSurface("/dev/null", 0, 0)
+ ctx = cairo.Context(surf)
+ pangoctx = pangocairo.CairoContext(ctx)
+ text_layout = pangoctx.create_layout()
+ text_layout.set_text(value)
+ self.__text_dimentions = text_layout.get_pixel_size()
+ del text_layout, pangoctx, ctx, surf
+
+ def do_size_request(self, requisition):
+ """Fill requisition with size occupied by the widget."""
+ width, height = self.__text_dimentions
+
+ # FIXME bogus values follows. will need to replace them with
+ # padding relative to font size and line border size
+ requisition.width = int(width+30)
+ requisition.height = int(height+40)
+
+ def do_size_allocate(self, allocation):
+ """Save zone allocated to the widget."""
+ self.allocation = allocation
+
+ def _get_label(self):
+ """Getter method for the label property"""
+ return self.__label
+
+ def _set_no_expose(self, value):
+ """setter for no_expose property"""
+ if self.__exposer and value:
+ self.disconnect(self.__exposer)
+ self.__exposer = None
+ elif (not self.__exposer) and (not value):
+ self.__exposer = self.connect("expose-event", self.__on_expose)
+
+ def _get_no_expose(self):
+ """getter for no_expose property"""
+ return not self.__exposer
+
+ no_expose = property(fset=_set_no_expose, fget=_get_no_expose,
+ doc="Whether the widget should handle exposition events or not.")
+
+ label = property(fget=_get_label, fset=_set_label,
+ doc="Text label which is to be painted on the top of the widget")
+
+gobject.type_register(TextBubble)
+
+
+# vim:set ts=4 sts=4 sw=4 et: