From a82d9228b66c914782b7cba6c971395be82313e8 Mon Sep 17 00:00:00 2001 From: simpoir Date: Mon, 16 Mar 2009 02:41:03 +0000 Subject: fixes following code review by Erick overlayer test and fixed non-working code accordingly --- diff --git a/src/sugar/graphics/window.py b/src/sugar/graphics/window.py index a3006a6..a17ebcc 100644 --- a/src/sugar/graphics/window.py +++ b/src/sugar/graphics/window.py @@ -101,10 +101,7 @@ class Window(gtk.Window): ## self.add(self._vbox) self._vbox.show() - self._overlayer = Overlayer() -## self._overlayer.inject(self._vbox) - self._overlayer.put(self._vbox, 0, 0) - self._overlayer._Overlayer__overlayed = self._vbox + self._overlayer = Overlayer(self._vbox) self.add(self._overlayer) self._overlayer.show() diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py index 915b11b..da8219e 100644 --- a/src/sugar/tutorius/actions.py +++ b/src/sugar/tutorius/actions.py @@ -126,11 +126,18 @@ class BubbleMessage(Action): Show the dialog """ # get or inject overlayer - self.overlay = ObjectStore().activity._overlayer #FIXME:handle subwin + self.overlay = ObjectStore().activity._overlayer + # FIXME: subwindows, are left to overlap this. This behaviour is + # undesirable. subwindows (i.e. child of top level windows) should be + # handled either by rendering over them, or by finding different way to + # draw the overlay. if not self._bubble: x, y = self.position - self._bubble = overlayer.TextBubble(text=self._message, tailpos=self._tailpos) #TODO: add tail handling + # TODO: tails are relative to tailpos. They should be relative to + # the speaking widget. Same of the bubble position. + self._bubble = overlayer.TextBubble(text=self._message, + tailpos=self._tailpos) self._bubble.show() self.overlay.put(self._bubble, x, y) self.overlay.queue_draw() @@ -140,8 +147,6 @@ class BubbleMessage(Action): Destroy the dialog """ if self._bubble: -## self.overlay.remove(self._bubble) -## self.overlay.bin_window.clear() self._bubble.destroy() self._bubble = None diff --git a/src/sugar/tutorius/overlayer.py b/src/sugar/tutorius/overlayer.py index 23a203b..c08ed4c 100644 --- a/src/sugar/tutorius/overlayer.py +++ b/src/sugar/tutorius/overlayer.py @@ -23,6 +23,8 @@ 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 @@ -38,12 +40,15 @@ 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): + def __init__(self, overlayed=None): gtk.Layout.__init__(self) - # no overlayed child yet - self.__overlayed = None + 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) @@ -61,7 +66,7 @@ class Overlayer(gtk.Layout): @param x the horizontal coordinate for positionning @param y the vertical coordinate for positionning """ - if hasattr(child, "no_expose"): + 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) @@ -79,12 +84,6 @@ class Overlayer(gtk.Layout): self.disconnect(self.__realizer) del self.__realizer - # use RGBA on overlayer so that when we paint we don't cover what's - # under - screen = self.get_screen() - rgba_map = screen.get_rgba_colormap() - self.set_colormap(rgba_map) # will fail if already realized - # app paintable will ensure that what we draw isn't erased by gtk self.parent.set_app_paintable(True) # the parent is composited, so we can access gtk's rendered buffer @@ -134,7 +133,7 @@ class Overlayer(gtk.Layout): # 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) + self._overlayed.set_size_request(allocation.width, allocation.height) class TextBubble(gtk.Widget): @@ -153,18 +152,18 @@ class TextBubble(gtk.Widget): """ gtk.Widget.__init__(self) - ##self.set_app_paintable(True) # else may be blank - # FIXME ensure previous call does not interfere with widget stacking + # 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.__exposer = None # refer to expose event handler self.label = text self.speaker = speaker self.tailpos = tailpos self.line_width = 5 - self.connect("expose-event", self.__on_expose) + self.__exposer = self.connect("expose-event", self.__on_expose) def draw_with_context(self, context): """ @@ -222,7 +221,7 @@ class TextBubble(gtk.Widget): context.fill() # text - # FIXME create layout in realize method + # 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() @@ -238,11 +237,36 @@ 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) def __on_expose(self, widget, event): """Redraw event callback.""" - # TODO: handle gtk window management in case there is no overlay ctx = self.window.cairo_create() self.draw_with_context(ctx) @@ -253,7 +277,8 @@ class TextBubble(gtk.Widget): """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. + # 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) @@ -279,14 +304,19 @@ class TextBubble(gtk.Widget): """Getter method for the label property""" return self.__label - def _set_will_expose(self, value): - """setter for will_expose property""" - if self.__exposer and not value: + def _set_no_expose(self, value): + """setter for no_expose property""" + if self.__exposer and value: self.disconnect(self.__exposer) - elif not (self.__exposer or value): - self.__exposer = self.connect(self.__on_expose) + 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_will_expose, + 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, diff --git a/src/sugar/tutorius/tests/overlaytests.py b/src/sugar/tutorius/tests/overlaytests.py new file mode 100644 index 0000000..b5fd209 --- /dev/null +++ b/src/sugar/tutorius/tests/overlaytests.py @@ -0,0 +1,115 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Simon Poirier +# +# 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 +""" +GUI Tests + +This module contains all the tests that pertain to the usage of the Tutorius +overlay mechanism used to display objects on top of the application. +""" + +import unittest + +import logging +import gtk, gobject +from sugar.tutorius.actions import Action, BubbleMessage +import sugar.tutorius.overlayer as overlayer + +class CanvasDrawable(object): + def __init__(self): + self._no_expose = False + self.exposition_count = 0 + def _set_no_expose(self, value): + self._no_expose = value + def draw_with_context(self, context): + self.exposition_count += 1 + no_expose = property(fset=_set_no_expose) + + +class OverlayerTest(unittest.TestCase): + def test_cairodrawable_iface(self): + """ + Quickly validates that all our cairo widgets have a minimal interface + implemented. + """ + drawables = [overlayer.TextBubble] + for widget in drawables: + for attr in filter(lambda s:s[0]!='_', dir(CanvasDrawable)): + assert hasattr(widget, attr), \ + "%s not implementing CanvasDrawable iface"%widget.__name__ + + + def test_drawn(self): + """ + Ensures a cairo widget draw method is called at least once in + a real gui app. + """ + win = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + + btn = gtk.Button() + btn.show() + overlay = overlayer.Overlayer(btn) + win.add(overlay) + # let's also try to draw substitute button label + lbl = overlayer.TextBubble("test!") + assert lbl.label == 'test!', \ + "label property mismatch" + btn.show() + lbl.show() + btn.add(lbl) + + lbl.no_expose = True + assert lbl.no_expose, "wrong no_expose evaluation" + lbl.no_expose = False + assert not lbl.no_expose, "wrong no_expose evaluation" + + + widget = overlayer.TextBubble("testing msg!", tailpos=(10,-20)) + widget.exposition_count = 0 + # override draw method + def counter(ctx, self=widget): + self.exposition_count += 1 + self.real_exposer(ctx) + widget.real_exposer = widget.draw_with_context + widget.draw_with_context = counter + # centering allows to test the blending with the label + overlay.put(widget, 50, 50) + widget.show() + assert widget.no_expose, \ + "Overlay should overide exposition handling of widget" + assert not lbl.no_expose, \ + "Non-overlayed cairo should expose as usual" + + # force widget realization + # the child is flagged to be redrawn, the overlay should redraw too. + win.set_default_size(100, 100) + win.show() + + while gtk.events_pending(): + gtk.main_iteration(block=False) + # visual validation: there should be 2 visible bubbles, one as label, + # one as overlay + import time + time.sleep(1) + # as x11 events are asynchronous, wait a bit before assuming it went + # wrong. + while gtk.events_pending(): + gtk.main_iteration(block=False) + assert widget.exposition_count>0, "overlay widget should expose" + + +if __name__ == "__main__": + unittest.main() -- cgit v0.9.1