From de3cb8c0c9ab1bb2215338e1757bb231331e1c6c Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 19 Mar 2009 20:20:40 +0000 Subject: 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 --- 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 +# +# 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() -- cgit v0.9.1