Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/sugar/tutorius
diff options
context:
space:
mode:
authorsimpoir <simpoir@Luyten.local>2009-03-16 02:41:03 (GMT)
committer simpoir <simpoir@Luyten.local>2009-03-16 02:41:03 (GMT)
commita82d9228b66c914782b7cba6c971395be82313e8 (patch)
tree2e99b85126efb609718e2efd7bf93806bce70b69 /src/sugar/tutorius
parent567021eac56855b16bde1ea2d103becf00552e18 (diff)
fixes following code review by Erick
overlayer test and fixed non-working code accordingly
Diffstat (limited to 'src/sugar/tutorius')
-rw-r--r--src/sugar/tutorius/actions.py13
-rw-r--r--src/sugar/tutorius/overlayer.py78
-rw-r--r--src/sugar/tutorius/tests/overlaytests.py115
3 files changed, 178 insertions, 28 deletions
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 <simpoir@gmail.com>
+#
+# 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()