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.py364
1 files changed, 270 insertions, 94 deletions
diff --git a/src/sugar/tutorius/overlayer.py b/src/sugar/tutorius/overlayer.py
index c08ed4c..12ea82f 100644
--- a/src/sugar/tutorius/overlayer.py
+++ b/src/sugar/tutorius/overlayer.py
@@ -1,6 +1,6 @@
"""
-This guy manages drawing of overlayed widgets. The class responsible for drawing
-management (Overlayer) and overlayable widgets are defined here.
+This module manages drawing of overlayed widgets. The class responsible for
+drawing management (Overlayer) and basic overlayable widgets are defined here.
"""
# Copyright (C) 2009, Tutorius.org
#
@@ -22,6 +22,20 @@ import gobject
import gtk
import cairo
import pangocairo
+from math import pi
+
+from sugar import profile
+
+# for easy profile access from cairo
+color = profile.get_color().get_stroke_color()
+xo_line_color = (int(color[1:3], 16)/255.0,
+ int(color[3:5], 16)/255.0,
+ int(color[5:7], 16)/255.0)
+color = profile.get_color().get_fill_color()
+xo_fill_color = (int(color[1:3], 16)/255.0,
+ int(color[3:5], 16)/255.0,
+ int(color[5:7], 16)/255.0)
+del color
# This is the CanvasDrawable protocol. Any widget wishing to be drawn on the
# overlay must implement it. See TextBubble for a sample implementation.
@@ -71,6 +85,9 @@ class Overlayer(gtk.Layout):
child.no_expose = True
gtk.Layout.put(self, child, x, y)
+ # be sure to redraw or the overlay may not show
+ self.queue_draw()
+
def __init_realized(self, widget, event):
"""
@@ -138,10 +155,10 @@ class Overlayer(gtk.Layout):
class TextBubble(gtk.Widget):
"""
- A CanvasDrawableWidget drawing a round textbox and a tail pointing
- to a specified widget.
+ A CanvasDrawableWidget drawing a round textbox and a tail pointing
+ to a specified widget.
"""
- def __init__(self, text, speaker=None, tailpos=None):
+ def __init__(self, text, speaker=None, tailpos=[0,0]):
"""
Creates a new cairo rendered text bubble.
@@ -156,14 +173,15 @@ class TextBubble(gtk.Widget):
# 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.padding = 20
- self.__exposer = self.connect("expose-event", self.__on_expose)
+ self._no_expose = False
+ self.__exposer = None
def draw_with_context(self, context):
"""
@@ -178,58 +196,55 @@ class TextBubble(gtk.Widget):
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)
+ # draw bubble tail if present
+ if self.tailpos != [0,0]:
+ context.move_to(xradius-width/4, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
- context.line_to(xradius+40, yradius)
+ context.line_to(xradius+width/4, yradius)
context.set_line_width(self.line_width)
- context.set_source_rgb(0.0, 0.0, 0.0)
+ context.set_source_rgb(*xo_line_color)
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)
+ # bubble border
+ context.move_to(width-self.padding, 0.0)
+ context.line_to(self.padding, 0.0)
+ context.arc_negative(self.padding, self.padding, self.padding,
+ 3*pi/2, pi)
+ context.line_to(0.0, height-self.padding)
+ context.arc_negative(self.padding, height-self.padding, self.padding,
+ pi, pi/2)
+ context.line_to(width-self.padding, height)
+ context.arc_negative(width-self.padding, height-self.padding,
+ self.padding, pi/2, 0)
+ context.line_to(width, self.padding)
+ context.arc_negative(width-self.padding, self.padding, self.padding,
+ 0.0, -pi/2)
+ context.set_line_width(self.line_width)
+ context.set_source_rgb(*xo_line_color)
+ context.stroke_preserve()
+ context.set_source_rgb(*xo_fill_color)
context.fill()
- # text
- # FIXME create text layout when setting text or in realize method
- context.set_source_rgb(0.0, 0.0, 0.0)
+ # bubble painting. Redrawing the inside after the tail will combine
+ if self.tailpos != [0,0]:
+ context.move_to(xradius-width/4, yradius)
+ context.line_to(self.tailpos[0], self.tailpos[1])
+ context.line_to(xradius+width/4, yradius)
+ context.set_line_width(self.line_width)
+ context.set_source_rgb(*xo_fill_color)
+ context.fill()
+
+ context.set_source_rgb(1.0, 1.0, 1.0)
pangoctx = pangocairo.CairoContext(context)
- text_layout = pangoctx.create_layout()
- text_layout.set_text(self.__label)
+ self._text_layout.set_markup(self.__label)
+ text_size = self._text_layout.get_pixel_size()
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)
+ int((self.allocation.width-text_size[0])/2),
+ int((self.allocation.height-text_size[1])/2))
+ pangoctx.show_layout(self._text_layout)
# work done. Be kind to next cairo widgets and reset matrix.
context.identity_matrix()
@@ -237,33 +252,10 @@ class TextBubble(gtk.Widget):
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)
+ if not self._no_expose:
+ self.__exposer = self.connect_after("expose-event", \
+ self.__on_expose)
def __on_expose(self, widget, event):
"""Redraw event callback."""
@@ -275,26 +267,25 @@ class TextBubble(gtk.Widget):
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)
+ self.__label = "<b>%s</b>"%value
+ if not self.parent:
+ return
+ ctx = self.parent.window.cairo_create()
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
+ self._text_layout = pangoctx.create_layout()
+ self._text_layout.set_markup(value)
+ del pangoctx, ctx#, surf
def do_size_request(self, requisition):
"""Fill requisition with size occupied by the widget."""
- width, height = self.__text_dimentions
+ ctx = self.parent.window.cairo_create()
+ pangoctx = pangocairo.CairoContext(ctx)
+ self._text_layout = pangoctx.create_layout()
+ self._text_layout.set_markup(self.__label)
- # 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)
+ width, height = self._text_layout.get_pixel_size()
+ requisition.width = int(width+2*self.padding)
+ requisition.height = int(height+2*self.padding)
def do_size_allocate(self, allocation):
"""Save zone allocated to the widget."""
@@ -302,19 +293,24 @@ class TextBubble(gtk.Widget):
def _get_label(self):
"""Getter method for the label property"""
- return self.__label
+ return self.__label[3:-4]
def _set_no_expose(self, value):
"""setter for no_expose property"""
+ self._no_expose = value
+ if not (self.flags() and gtk.REALIZED):
+ return
+
if self.__exposer and value:
- self.disconnect(self.__exposer)
+ self.parent.disconnect(self.__exposer)
self.__exposer = None
elif (not self.__exposer) and (not value):
- self.__exposer = self.connect("expose-event", self.__on_expose)
+ self.__exposer = self.parent.connect_after("expose-event",
+ self.__on_expose)
def _get_no_expose(self):
"""getter for no_expose property"""
- return not self.__exposer
+ return self._no_expose
no_expose = property(fset=_set_no_expose, fget=_get_no_expose,
doc="Whether the widget should handle exposition events or not.")
@@ -324,5 +320,185 @@ class TextBubble(gtk.Widget):
gobject.type_register(TextBubble)
+class Rectangle(gtk.Widget):
+ """
+ A CanvasDrawableWidget drawing a rectangle over a specified widget.
+ """
+ def __init__(self, widget, color):
+ """
+ Creates a new Rectangle
+
+ @param widget the widget to cover
+ @param color the color of the rectangle, as a 4-tuple RGBA
+ """
+ gtk.Widget.__init__(self)
+
+ self.covered = widget
+ self.color = color
+
+ 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.
+ """
+ if self.covered is None:
+ # nothing to hide, no coordinates, no drawing
+ return
+ mask_alloc = self.covered.get_allocation()
+ x, y = self.covered.translate_coordinates(self.parent, 0, 0)
+
+ context.rectangle(x, y, mask_alloc.width, mask_alloc.height)
+ context.set_source_rgba(*self.color)
+ context.fill()
+
+ def do_realize(self):
+ """ Setup gdk window creation. """
+ self.set_flags(gtk.REALIZED | gtk.NO_WINDOW)
+
+ self.window = self.get_parent_window()
+ if not isinstance(self.parent, Overlayer):
+ assert False, "%s should not realize" % type(self).__name__
+ print "Danger, Will Robinson! Rectangle parent is not Overlayer"
+
+ def __on_expose(self, widget, event):
+ """Redraw event callback."""
+ assert False, "%s wasn't meant to be exposed by gtk" % \
+ type(self).__name__
+ ctx = self.window.cairo_create()
+
+ self.draw_with_context(ctx)
+
+ return True
+
+ def do_size_request(self, requisition):
+ """Fill requisition with size occupied by the masked widget."""
+ # This is a bit pointless, as this will always ignore allocation and
+ # be rendered directly on overlay, but for sanity, let's put some values
+ # in there.
+ if not self.covered:
+ requisition.width = 0
+ requisition.height = 0
+ return
+
+ masked_alloc = self.covered.get_allocation()
+ requisition.width = masked_alloc.width
+ requisition.height = masked_alloc.height
+
+ def do_size_allocate(self, allocation):
+ """Save zone allocated to the widget."""
+ self.allocation = allocation
+
+ 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.")
+gobject.type_register(Rectangle)
+
+class Mask(gtk.EventBox):
+ """
+ A CanvasDrawableWidget drawing a rectangle over a specified widget.
+ """
+ def __init__(self, catch_events=False, pass_thru=()):
+ """
+ Creates a new Rectangle
+
+ @param catch_events whether the Mask should catch events
+ @param pass_thru the widgets that "punch holes" through this Mask.
+ Events will pass through to those widgets.
+ """
+ gtk.EventBox.__init__(self)
+ self.no_expose = True # ignored
+ self._catch_events = False
+ self.catch_events = catch_events
+ self.pass_thru = list(pass_thru)
+
+ def __del__(self):
+ for widget in self.pass_thru:
+ widget.drag_unhighlight()
+
+ def mask(self, widget):
+ """
+ Remove the widget from the unmasked list.
+ @param widget a widget to remask
+ """
+ assert widget in self.pass_thru, \
+ "trying to mask already masked widget"
+ self.pass_thru.remove(widget)
+ widget.drag_unhighlight()
+
+ def unmask(self, widget):
+ """
+ Add to the unmasked list the widget passed.
+ A hole will be punched through the mask at that widget's position.
+ @param widget a widget to unmask
+ """
+ if widget not in self.pass_thru:
+ widget.drag_highlight()
+ self.pass_thru.append(widget)
+
+
+ def set_catch_events(self, do_catch):
+ """Sets whether the mask catches events of widgets under it"""
+ if bool(self._catch_events) ^ bool(do_catch):
+ if do_catch:
+ self._catch_events = True
+ self.grab_add()
+ else:
+ self.grab_remove()
+ self._catch_events = False
+
+ def get_catch_events(self):
+ """Gets whether the mask catches events of widgets under it"""
+ return bool(self._catch_handle)
+
+ catch_events = property(fset=set_catch_events, fget=get_catch_events)
+
+ 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.
+ """
+ # Fill parent container
+ mask_alloc = self.parent.get_allocation()
+ oldrule = context.get_fill_rule()
+ context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
+ x, y = self.translate_coordinates(self.parent, 0, 0)
+
+ context.rectangle(x, y, mask_alloc.width, mask_alloc.height)
+ for hole in self.pass_thru:
+ alloc = hole.get_allocation()
+ x, y = hole.translate_coordinates(self.parent, 0, 0)
+ context.rectangle(x, y, alloc.width, alloc.height)
+ context.set_source_rgba(0, 0, 0, 0.7)
+ context.fill()
+ context.set_fill_rule(oldrule)
+
+ def do_size_request(self, requisition):
+ """Fill requisition with size occupied by the masked widget."""
+ # This is required for the event box to span across all the parent.
+ alloc = self.parent.get_allocation()
+ requisition.width = alloc.width
+ requisition.height = alloc.height
+
+ def do_size_allocate(self, allocation):
+ """Save zone allocated to the widget."""
+ self.allocation = allocation
+
+gobject.type_register(Mask)
+
# vim:set ts=4 sts=4 sw=4 et: