Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormike <michael.jmontcalm@gmail.com>2009-03-19 20:20:40 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-03-19 20:20:40 (GMT)
commitde3cb8c0c9ab1bb2215338e1757bb231331e1c6c (patch)
tree98d99a66d829e5b94ba7392ee813e13b4e605ca9
parent3d41146af5f136d156b5a1f0c65c553612fbd4b2 (diff)
parente20cfb534e276fa7762980bbdd6d633a7ce99ccc (diff)
Merge branch 'tutorial_toolkit' into mike
Conflicts: source/activities/Writus.activity/TAbiWordActivity.py source/external/source/sugar-toolkit/src/sugar/graphics/window.py source/external/source/sugar-toolkit/src/sugar/tutorius/Makefile.am source/external/source/sugar-toolkit/src/sugar/tutorius/actions.py source/external/source/sugar-toolkit/src/sugar/tutorius/overlayer.py source/external/source/sugar-toolkit/src/sugar/tutorius/services.py source/external/source/sugar-toolkit/src/sugar/tutorius/tests/coretests.py source/external/source/sugar-toolkit/src/sugar/tutorius/tests/run-tests.py
-rw-r--r--src/sugar/activity/activity.py15
-rw-r--r--src/sugar/graphics/window.py5
-rw-r--r--src/sugar/tutorius/Makefile.am3
-rw-r--r--src/sugar/tutorius/actions.py35
-rw-r--r--src/sugar/tutorius/core.py3
-rw-r--r--src/sugar/tutorius/editor.py115
-rw-r--r--src/sugar/tutorius/gtkutils.py18
-rw-r--r--src/sugar/tutorius/overlayer.py250
-rw-r--r--src/sugar/tutorius/services.py2
-rw-r--r--src/sugar/tutorius/tests/coretests.py14
-rw-r--r--src/sugar/tutorius/tests/overlaytests.py115
-rwxr-xr-xsrc/sugar/tutorius/tests/run-tests.py38
12 files changed, 466 insertions, 147 deletions
diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py
index c5dca45..21e38f6 100644
--- a/src/sugar/activity/activity.py
+++ b/src/sugar/activity/activity.py
@@ -76,6 +76,7 @@ from sugar.graphics.xocolor import XoColor
from sugar.datastore import datastore
from sugar.session import XSMPClient
from sugar import wm
+from sugar.tutorius.services import ObjectStore
_ = lambda msg: gettext.dgettext('sugar-toolkit', msg)
@@ -181,18 +182,6 @@ class ActivityToolbar(gtk.Toolbar):
self._updating_share = False
- def __tutorial_changed_cb(self, combo):
- if self._current_tutorial:
- self._current_tutorial.detach()
-
- model = self.tutorials.combo.get_model()
- it = self.tutorials.combo.get_active_iter()
- (key, ) = model.get(it, 0)
- t = self._activity.get_tutorials.get(key,None)
- if t:
- self._current_tutorial = t
- self._current_tutorial.attach(self._activity)
-
def __share_changed_cb(self, combo):
if self._updating_share:
return
@@ -508,6 +497,8 @@ class Activity(Window, gtk.Container):
"""
Window.__init__(self)
+ ObjectStore().activity = self
+
# process titles will only show 15 characters
# but they get truncated anyway so if more characters
# are supported in the future we will get a better view
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/Makefile.am b/src/sugar/tutorius/Makefile.am
index 6fd32c7..1fb11e1 100644
--- a/src/sugar/tutorius/Makefile.am
+++ b/src/sugar/tutorius/Makefile.am
@@ -7,4 +7,5 @@ sugar_PYTHON = \
gtkutils.py \
filters.py \
services.py \
- overlayer.py
+ overlayer.py \
+ editor.py
diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py
index 7ecf86b..12de298 100644
--- a/src/sugar/tutorius/actions.py
+++ b/src/sugar/tutorius/actions.py
@@ -16,10 +16,13 @@
"""
This module defines Actions that can be done and undone on a state
"""
+from gettext import gettext as _
from sugar.tutorius import gtkutils
from dialog import TutoriusDialog
import overlayer
+from sugar.tutorius.editor import WidgetIdentifier
+from sugar.tutorius.services import ObjectStore
class Action(object):
@@ -125,11 +128,18 @@ class BubbleMessage(Action):
Show the dialog
"""
# get or inject overlayer
- self.overlay = gtkutils.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
+ # 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()
@@ -139,8 +149,25 @@ 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
+
+class WidgetIdentifyAction(Action):
+ def __init__(self):
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = WidgetIdentifier(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ self._dialog.destroy()
+
diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py
index 1bf81db..8af59ff 100644
--- a/src/sugar/tutorius/core.py
+++ b/src/sugar/tutorius/core.py
@@ -27,6 +27,7 @@ import copy
from sugar.tutorius.dialog import TutoriusDialog
from sugar.tutorius.gtkutils import find_widget
+from sugar.tutorius.services import ObjectStore
logger = logging.getLogger("tutorius")
@@ -61,6 +62,8 @@ class Tutorial (object):
if self.activity:
self.detach()
self.activity = activity
+ ObjectStore().activity = activity
+ ObjectStore().tutorial = self
self.state_machine.set_state("INIT")
def detach(self):
diff --git a/src/sugar/tutorius/editor.py b/src/sugar/tutorius/editor.py
new file mode 100644
index 0000000..1a1eb61
--- /dev/null
+++ b/src/sugar/tutorius/editor.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2009, Tutorius.org
+# Greatly influenced by sugar/activity/namingalert.py
+#
+# 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 gtk
+import gobject
+import hippo
+import gconf
+
+from gettext import gettext as _
+
+class WidgetIdentifier(gtk.Window):
+ """
+ Tool that allows identifying widgets
+ """
+ __gtype_name__ = 'TutoriusWidgetIdentifier'
+
+ def __init__(self, activity):
+ gtk.Window.__init__(self)
+
+ self._activity = activity
+ self._handlers = []
+
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(False)
+
+ self.connect('realize', self.__realize_cb)
+
+ self._expander = gtk.Expander(_("Widget Identifier"))
+ self._expander.set_expanded(True)
+ self.add(self._expander)
+ self._expander.connect("notify::expanded", self.__expander_cb)
+
+ self._expander.show()
+
+ vbox = gtk.VBox()
+ self._expander.add(vbox)
+ vbox.show()
+
+
+ self.logview = gtk.TextView()
+ self.logview.set_editable(False)
+ self.logview.set_cursor_visible(False)
+ self.logview.set_wrap_mode(gtk.WRAP_NONE)
+ self._textbuffer = self.logview.get_buffer()
+
+ sw = gtk.ScrolledWindow()
+ sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ sw.add(self.logview)
+ self.logview.show()
+
+ vbox.pack_start(sw)
+ sw.show()
+
+ from sugar.tutorius.gtkutils import register_signals_numbered
+ self._handlers = register_signals_numbered(self._activity, self._handle_events)
+
+ def __expander_cb(self, *args):
+ if self._expander.get_expanded():
+ self.__move_expanded()
+ else:
+ self.__move_collapsed()
+
+ def __move_expanded(self):
+ width = 400
+ height = 300
+ ww = gtk.gdk.screen_width()
+ wh = gtk.gdk.screen_height()
+
+ self.set_size_request(width, height)
+ self.move((ww-width)/2, wh-height)
+
+ def __move_collapsed(self):
+ width = 150
+ height = 40
+ ww = gtk.gdk.screen_width()
+ wh = gtk.gdk.screen_height()
+
+ self.set_size_request(width, height)
+ self.move((ww-width)/2, wh-height)
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+ self.__move_expanded()
+
+ def _disconnect_handlers(self):
+ for widget, handlerid in self._handlers:
+ widget.handler_disconnect(handlerid)
+ self._handlers = []
+
+ def _handle_events(self,*args):
+ sig, name = args[-1]
+ text = "\r\n".join(
+ (["%s event received from %s" % (sig, name)] +
+ self._textbuffer.get_text(*(self._textbuffer.get_bounds())
+ ).split("\r\n"))[:80]
+ )
+ self._textbuffer.set_text(text)
+
+
diff --git a/src/sugar/tutorius/gtkutils.py b/src/sugar/tutorius/gtkutils.py
index 450872d..c5c1e12 100644
--- a/src/sugar/tutorius/gtkutils.py
+++ b/src/sugar/tutorius/gtkutils.py
@@ -17,6 +17,7 @@
"""
Utility classes and functions that are gtk related
"""
+import gtk
def activity(activity=None, singleton=[]):
if activity:
@@ -113,7 +114,7 @@ def register_signals_numbered(target, handler, prefix="0", max_depth=None):
ret+=register_signals_numbered(child, handler, pre, dep)
#register events on the target if a widget XXX necessary to check this?
if isinstance(target, gtk.Widget):
- for sig in Tutorial.EVENTS:
+ for sig in EVENTS:
try:
ret.append( \
(target, target.connect(sig, handler, (sig, prefix) ))\
@@ -123,7 +124,7 @@ def register_signals_numbered(target, handler, prefix="0", max_depth=None):
return ret
-def register_signals(self, target, handler, prefix=None, max_depth=None):
+def register_signals(target, handler, prefix=None, max_depth=None):
"""
Recursive function to register event handlers on an target
and it's children. The event handler is called with an extra
@@ -131,19 +132,20 @@ def register_signals(self, target, handler, prefix=None, max_depth=None):
the FQDN-style name of the target that triggered the event.
This function registers all of the events listed in
- Tutorial.EVENTS and omits widgets with a name matching
- Tutorial.IGNORED_WIDGETS from the name hierarchy.
+ EVENTS and omits widgets with a name matching
+ IGNORED_WIDGETS from the name hierarchy.
Example arg tuple added:
("focus", "Activity.Toolbox.Bold")
Side effects:
-Handlers connected on the various targets
- -Handler ID's stored in self.handlers
@param target the target to recurse on
@param handler the handler function to connect
@param prefix name prepended to the target name to form a chain
@param max_depth maximum recursion depth, None for infinity
+
+ @returns list of (object, handler_id)
"""
ret = []
#Gtk Containers have a get_children() function
@@ -154,16 +156,16 @@ def register_signals(self, target, handler, prefix=None, max_depth=None):
#Recurse with a prefix on all children
pre = ".".join( \
[p for p in (prefix, target.get_name()) \
- if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
+ if not (p is None or p in IGNORED_WIDGETS)] \
)
ret += register_signals(child, handler, pre, max_depth-1)
name = ".".join( \
[p for p in (prefix, target.get_name()) \
- if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
+ if not (p is None or p in IGNORED_WIDGETS)] \
)
#register events on the target if a widget XXX necessary to check this?
if isinstance(target, gtk.Widget):
- for sig in Tutorial.EVENTS:
+ for sig in EVENTS:
try:
ret.append( \
(target, target.connect(sig, handler, (sig, name) )) \
diff --git a/src/sugar/tutorius/overlayer.py b/src/sugar/tutorius/overlayer.py
index 0da1841..c08ed4c 100644
--- a/src/sugar/tutorius/overlayer.py
+++ b/src/sugar/tutorius/overlayer.py
@@ -23,75 +23,54 @@ import gtk
import cairo
import pangocairo
-class CanvasDrawable(object):
- """Defines the CanvasDrawable protocol"""
- should_expose = None
- def drawWithContext(self, context): pass
-
+# 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):
+ 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)
-## win.connect("expose-event", self.__transparent_overlay)
self.__realizer = self.connect("expose-event", self.__init_realized)
self.connect("size-allocate", self.__size_allocate)
self.show()
-## win.show_all()
-
-## win.connect_after("expose-event", self.__expose_overlay)
-
- def reparent(self, parent):
- """Reparent this widget as child of parent instead."""
- self.__realizer = self.connect("expose-event", self.__init_realized)
- gtk.Layout.reparent(self, parent)
-
- def inject(self, child):
- """
- Insert this widget at "child" level so that it can overlay "child".
- """
- assert not self.__overlayed, "trying to reinject while already injected"
- parent = child.parent
- self.__overlayed = child
- child.reparent(self)
- parent.add(self)
- self.__realizer = parent.connect("expose-event", self.__init_realized)
+ 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, "should_expose"):
+ if hasattr(child, "draw_with_context"):
# if the widget has the CanvasDrawable protocol, use it.
- # protocol is defined as
- # should_expose property with setter to flag as not drawn using
- # expose-events
- # drawWithContext(context) method to pass a context to draw with.
- child.should_expose = True
+ child.no_expose = True
gtk.Layout.put(self, child, x, y)
- def eject(self):
- """
- Remove this widget from the widget hierarchy without affecting its
- children.
- """
- parent = self.parent
- parent.disconnect(self.__render_handle)
- assert hasattr(parent, "remove"), "parent should be a container"
- self.parent.remove(self)
- self.__overlayed.reparent(parent)
- self.__overlayed = None
def __init_realized(self, widget, event):
"""
@@ -105,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,11 +107,11 @@ class Overlayer(gtk.Layout):
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()
+ 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
@@ -146,41 +119,53 @@ class Overlayer(gtk.Layout):
#draw overlay
for drawn_child in self.get_children():
- if hasattr(drawn_child, "drawWithContext"):
- drawn_child.drawWithContext(ctx)
+ 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"""
+ """
+ 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)
+ 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 associate with the bubble tail
- @param tailpos an optional position for the tail if no speaker
+ @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)
- ##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.label = text
self.speaker = speaker
self.tailpos = tailpos
- self.lineWidth = 5
+ self.line_width = 5
- self.connect("expose-event", self.__on_expose)
+ self.__exposer = self.connect("expose-event", self.__on_expose)
- def drawWithContext(self, context):
+ 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
@@ -191,46 +176,58 @@ class TextBubble(gtk.Widget):
height = self.allocation.height
xradius = width/2
yradius = height/2
- width -= self.lineWidth
- height -= self.lineWidth
+ 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:
-## speaker_alloc = self.speaker.get_allocation()
context.move_to(xradius-40, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
-## context.line_to(speaker_alloc.x-self.allocation.x,
-## speaker_alloc.y - self.allocation.y)
context.line_to(xradius+40, yradius)
- context.set_source_rgb(1.0, 1.0, 0.0)
- context.fill_preserve()
- context.set_line_width(self.lineWidth)
+ context.set_line_width(self.line_width)
context.set_source_rgb(0.0, 0.0, 0.0)
- context.stroke()
-
- # bubble
- context.move_to(self.lineWidth, yradius)
- context.curve_to(self.lineWidth, self.lineWidth,
- self.lineWidth, self.lineWidth, xradius, self.lineWidth)
- context.curve_to(width, self.lineWidth,
- width, self.lineWidth, width, yradius)
+ 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.lineWidth, height,
- self.lineWidth, height, self.lineWidth, yradius)
+ 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_preserve()
- context.set_line_width(self.lineWidth)
- context.set_source_rgb(0.0, 0.0, 0.0)
- context.stroke()
+ 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()
text_layout.set_text(self.__label)
- pangoctx.move_to(int((self.allocation.width-self.__text_dimentions[0])/2),
+ 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)
@@ -238,36 +235,50 @@ class TextBubble(gtk.Widget):
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."""
- # TODO
ctx = self.window.cairo_create()
- # set drawing region. Useless since this widget has its own window.
-## region = gtk.gdk.region_rectangle(self.allocation)
-## region.intersect(gtk.gdk.region_rectangle(event.area))
-## ctx.region(region)
-## ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
-## ctx.clip()
-
- ##import pdb; pdb.set_trace()
- ##ctx.set_operator(cairo.OPERATOR_CLEAR)
- ##ctx.paint()
- ##ctx.set_operator(cairo.OPERATOR_OVER)
-
- self.drawWithContext(ctx)
+ self.draw_with_context(ctx)
return True
def _set_label(self, value):
"""Sets the label and flags the widget to be redrawn."""
- # yes we use the setter
self.__label = value
# FIXME hack to calculate size. necessary because may not have been
- # realized
+ # 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)
@@ -277,29 +288,38 @@ class TextBubble(gtk.Widget):
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
+ # 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_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
- should_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,\
+ 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)
diff --git a/src/sugar/tutorius/services.py b/src/sugar/tutorius/services.py
index 9627282..467eca0 100644
--- a/src/sugar/tutorius/services.py
+++ b/src/sugar/tutorius/services.py
@@ -32,6 +32,8 @@ class ObjectStore(object):
if not ObjectStore.instance:
ObjectStore.instance = ObjectStore.__ObjectStore()
+ return ObjectStore.instance
+
#End Singleton code
class __ObjectStore(object):
"""
diff --git a/src/sugar/tutorius/tests/coretests.py b/src/sugar/tutorius/tests/coretests.py
index af3f02c..a28880f 100644
--- a/src/sugar/tutorius/tests/coretests.py
+++ b/src/sugar/tutorius/tests/coretests.py
@@ -108,6 +108,20 @@ class FakeEventFilter(TriggerEventFilter):
def _inner_cb(self, event_filter):
self.toggle_on_callback = not self.toggle_on_callback
self.tutorial.set_state(event_filter.get_next_state())
+class BaseActionTests(unittest.TestCase):
+ def test_do_unimplemented(self):
+ act = Action()
+ try:
+ act.do()
+ assert False, "do() should trigger a NotImplemented"
+ except NotImplementedError:
+ assert True, "do() should trigger a NotImplemented"
+
+ def test_undo(self):
+ act = Action()
+ act.undo()
+ assert True, "undo() should never fail on the base action"
+
class OnceWrapperTests(unittest.TestCase):
def test_onceaction_toggle(self):
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()
diff --git a/src/sugar/tutorius/tests/run-tests.py b/src/sugar/tutorius/tests/run-tests.py
index 74efd64..db10c54 100755
--- a/src/sugar/tutorius/tests/run-tests.py
+++ b/src/sugar/tutorius/tests/run-tests.py
@@ -2,11 +2,43 @@
# This is a dumb script to run tests on the sugar-jhbuild installed files
# The path added is the default path for the jhbuild build
+INSTALL_PATH="../../../../../../install/lib/python2.5/site-packages/"
+
import os, sys
sys.path.insert(0,
- os.path.abspath("../../../../../../install/lib/python2.5/site-packages/")
+ os.path.abspath(INSTALL_PATH)
)
+
+FULL_PATH = os.path.join(INSTALL_PATH,"sugar/tutorius")
+GLOB_PATH = os.path.join(FULL_PATH,"*.py")
import unittest
-from coretests import *
+from glob import glob
+
+import sys
+if __name__=='__main__':
+ if "--coverage" in sys.argv:
+ sys.argv=[arg for arg in sys.argv if arg != "--coverage"]
+ import coverage
+ coverage.erase()
+ #coverage.exclude('raise NotImplementedError')
+ coverage.start()
+
+ import coretests
+ import servicestests
+
+
+ suite = unittest.TestSuite()
+ suite.addTests(unittest.findTestCases(coretests))
+ suite.addTests(unittest.findTestCases(servicestests))
+
+ runner = unittest.TextTestRunner()
+ runner.run(suite)
+
+ coverage.stop()
+ coverage.report(glob(GLOB_PATH))
+ coverage.erase()
+ else:
+ from coretests import *
+ from servicestests import *
-unittest.main()
+ unittest.main()