diff options
Diffstat (limited to 'src/sugar/tutorius/overlayer.py')
-rw-r--r-- | src/sugar/tutorius/overlayer.py | 364 |
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: |