Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--configure.ac1
-rw-r--r--src/sugar/tutorius/actions.py138
-rw-r--r--src/sugar/tutorius/bundler.py2
-rw-r--r--src/sugar/tutorius/constraints.py189
-rw-r--r--src/sugar/tutorius/core.py20
-rw-r--r--src/sugar/tutorius/editor.py257
-rw-r--r--src/sugar/tutorius/filters.py73
-rw-r--r--src/sugar/tutorius/gtkutils.py95
-rw-r--r--src/sugar/tutorius/properties.py204
-rw-r--r--src/sugar/tutorius/tests/actiontests.py173
-rw-r--r--src/sugar/tutorius/tests/constraintstests.py211
-rw-r--r--src/sugar/tutorius/tests/coretests.py174
-rw-r--r--src/sugar/tutorius/tests/filterstests.py200
-rw-r--r--src/sugar/tutorius/tests/gtkutilstests.py91
-rw-r--r--src/sugar/tutorius/tests/linear_creatortests.py16
-rw-r--r--src/sugar/tutorius/tests/propertiestests.py348
-rw-r--r--src/sugar/tutorius/tests/uamtests.py61
-rw-r--r--src/sugar/tutorius/uam/Makefile.am5
-rw-r--r--src/sugar/tutorius/uam/__init__.py88
-rw-r--r--src/sugar/tutorius/uam/gobjectparser.py27
-rw-r--r--src/sugar/tutorius/uam/gtkparser.py44
21 files changed, 2283 insertions, 134 deletions
diff --git a/configure.ac b/configure.ac
index 2f473cb..66e139b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -42,6 +42,7 @@ src/sugar/bundle/Makefile
src/sugar/graphics/Makefile
src/sugar/presence/Makefile
src/sugar/tutorius/Makefile
+src/sugar/tutorius/uam/Makefile
src/sugar/datastore/Makefile
po/Makefile.in
])
diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py
index 12de298..ad91fb4 100644
--- a/src/sugar/tutorius/actions.py
+++ b/src/sugar/tutorius/actions.py
@@ -42,6 +42,13 @@ class Action(object):
"""
pass #Should raise NotImplemented?
+ def get_properties(self):
+ if not hasattr(self, "_props") or self._props is None:
+ self._props = []
+ for i in dir(self.__class__):
+ if type(getattr(self.__class__,i)) is property:
+ self._props.append(i)
+ return self._props
class OnceWrapper(object):
"""
@@ -71,6 +78,9 @@ class OnceWrapper(object):
if self._need_undo:
self._action.undo()
self._need_undo = False
+
+ def get_properties(self):
+ return self._action.get_properties()
class DialogMessage(Action):
"""
@@ -82,9 +92,25 @@ class DialogMessage(Action):
def __init__(self, message, pos=[0,0]):
super(DialogMessage, self).__init__()
self._message = message
- self.position = pos
+ self._position = pos
self._dialog = None
+ def set_message(self, msg):
+ self._message = msg
+
+ def get_message(self):
+ return self._message
+
+ message = property(fget=get_message, fset=set_message)
+
+ def set_pos(self, x, y):
+ self._position = [x, y]
+
+ def get_pos(self):
+ return self._position
+
+ position = property(fget=get_pos, fset=set_pos)
+
def do(self):
"""
Show the dialog
@@ -115,14 +141,31 @@ class BubbleMessage(Action):
def __init__(self, message, pos=[0,0], speaker=None, tailpos=None):
Action.__init__(self)
self._message = message
- self.position = pos
+ self._position = pos
self.overlay = None
self._bubble = None
self._speaker = None
self._tailpos = tailpos
+ def set_message(self, msg):
+ self._message = msg
+ def get_message(self):
+ return self._message
+ message = property(fget=get_message, fset=set_message, doc="Message displayed to the user")
+ def set_pos(self, x, y):
+ self._position = [x, y]
+ def get_pos(self):
+ return self.position
+ position = property(fget=get_pos, fset=set_pos, doc="Position in [x, y] on the screen")
+
+ def set_tail_pos(self, x, y):
+ self._tailpos = [x, y]
+ def get_tail_pos(self):
+ return self._tailpos
+ tail_pos = property(fget=get_tail_pos, fset=set_tail_pos, doc="Position the tail of the bubble must point to")
+
def do(self):
"""
Show the dialog
@@ -135,7 +178,7 @@ class BubbleMessage(Action):
# draw the overlay.
if not self._bubble:
- x, y = self.position
+ x, y = self._position
# 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,
@@ -170,4 +213,93 @@ class WidgetIdentifyAction(Action):
if self._dialog:
self._dialog.destroy()
+class ChainAction(Action):
+ """Utility class to allow executing actions in a specific order"""
+ def __init__(self, *actions):
+ """ChainAction(action1, ... ) builds a chain of actions"""
+ self._actions = actions
+
+ def do(self,**kwargs):
+ """do() each action in the chain"""
+ for act in self._actions:
+ act.do(**kwargs)
+
+ def undo(self):
+ """undo() each action in the chain, starting with the last"""
+ for act in reversed(self._actions):
+ act.undo()
+
+class DisableWidgetAction(Action):
+ def __init__(self, target):
+ """Constructor
+ @param target target treeish
+ """
+ Action.__init__(self)
+ self._target = target
+ self._widget = None
+ def do(self):
+ """Action do"""
+ os = ObjectStore()
+ if os.activity:
+ self._widget = gtkutils.find_widget(os.activity, self._target)
+ if self._widget:
+ self._widget.set_sensitive(False)
+
+ def undo(self):
+ """Action undo"""
+ if self._widget:
+ self._widget.set_sensitive(True)
+
+class TypeTextAction(Action):
+ """
+ Simulate a user typing text in a widget
+ Work on any widget that implements a insert_text method
+
+ @param widget The treehish representation of the widget
+ @param text the text that is typed
+ """
+ def __init__(self, widget, text):
+ Action.__init__(self)
+
+ self._widget = widget
+ self._text = text
+
+ def do(self, **kwargs):
+ """
+ Type the text
+ """
+ widget = gtkutils.find_widget(ObjectStore().activity, self._widget)
+ if hasattr(widget, "insert_text"):
+ widget.insert_text(self._text, -1)
+
+ def undo(self):
+ """
+ no undo
+ """
+ pass
+
+class ClickAction(Action):
+ """
+ Action that simulate a click on a widget
+ Work on any widget that implements a clicked() method
+
+ @param widget The threehish representation of the widget
+ """
+ def __init__(self, widget):
+ Action.__init__(self)
+ self._widget = widget
+
+ def do(self):
+ """
+ click the widget
+ """
+ widget = gtkutils.find_widget(ObjectStore().activity, self._widget)
+ if hasattr(widget, "clicked"):
+ widget.clicked()
+
+ def undo(self):
+ """
+ No undo
+ """
+ pass
diff --git a/src/sugar/tutorius/bundler.py b/src/sugar/tutorius/bundler.py
index d089c35..8712c86 100644
--- a/src/sugar/tutorius/bundler.py
+++ b/src/sugar/tutorius/bundler.py
@@ -363,4 +363,4 @@ class TutorialBundler:
return False
else:
logger.debug("file is a directory :"+file)
- return False \ No newline at end of file
+ return False
diff --git a/src/sugar/tutorius/constraints.py b/src/sugar/tutorius/constraints.py
new file mode 100644
index 0000000..a666ecb
--- /dev/null
+++ b/src/sugar/tutorius/constraints.py
@@ -0,0 +1,189 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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
+"""
+Constraints
+
+Defines a set of constraints with their related errors. These constraints are
+made to be used inside TutoriusProperties in order to limit the values that
+they might take. They can also be used to enforce a particular format or type
+for some properties.
+"""
+
+# For the File Constraint
+import os
+
+class Constraint():
+ """
+ Basic block for defining constraints on a TutoriusProperty. Every class
+ inheriting from Constraint will have a validate function that will be
+ executed when the property's value is to be changed.
+ """
+ def validate(self, value):
+ """
+ This function receives the value that is proposed as a new value for
+ the property. It needs to raise an Error in the case where the value
+ does not respect this constraint.
+ """
+ raise NotImplementedError("Unable to validate a base Constraint")
+
+class ValueConstraint(Constraint):
+ """
+ A value constraint contains a _limit member that can be used in a children
+ class as a basic value. See UpperLimitConstraint for an exemple.
+ """
+ def __init__(self, limit):
+ self.limit = limit
+
+class UpperLimitConstraintError(Exception):
+ pass
+
+class UpperLimitConstraint(ValueConstraint):
+ def validate(self, value):
+ """
+ Evaluates whether the given value is smaller than the limit.
+
+ @raise UpperLimitConstraintError When the value is strictly higher than
+ the limit.
+ """
+ if self.limit is not None:
+ if self.limit >= value:
+ return
+ raise UpperLimitConstraintError()
+ return
+
+class LowerLimitConstraintError(Exception):
+ pass
+
+class LowerLimitConstraint(ValueConstraint):
+ def validate(self, value):
+ """
+ If the value is lower than the limit, this function raises an error.
+
+ @raise LowerLimitConstraintError When the value is strictly smaller
+ than the limit.
+ """
+ if self.limit is not None:
+ if value >= self.limit:
+ return
+ raise LowerLimitConstraintError()
+ return
+
+class SizeConstraintError(Exception):
+ pass
+
+class SizeConstraint(ValueConstraint):
+ def validate(self, value):
+ """
+ Evaluate whether a given object is smaller than the given size when
+ run through len(). Great for string, lists and the like. ;)
+
+ @raise SizeConstraintError If the length of the value is strictly
+ bigger than the limit.
+ """
+ if self.limit is not None:
+ if self.limit > len(value):
+ return
+ raise SizeConstraintError("Setter : trying to set value of length %d while limit is %d"%(len(value), self.limit))
+ return
+
+class ColorConstraintError(Exception):
+ pass
+
+class ColorArraySizeError(ColorConstraintError):
+ pass
+
+class ColorTypeError(ColorConstraintError):
+ pass
+
+class ColorValueError(ColorConstraintError):
+ pass
+
+class ColorConstraint(Constraint):
+ """
+ Validates that the value is an array of size 3 with three numbers between
+ 0 and 255 (inclusively) in it.
+
+ """
+ def validate(self, value):
+ if len(value) != 3:
+ raise ColorArraySizeError("The value is not an array of size 3")
+
+ if not (type(value[0]) == type(22) and type(value[1]) == type(22) and type(value[2]) == type(22)):
+ raise ColorTypeError("Not all the elements of the array are integers")
+
+ if value[0] > 255 or value[0] <0:
+ raise ColorValueError("Red value is not between 0 and 255")
+
+ if value[1] > 255 or value[1] <0:
+ raise ColorValueError("Green value is not between 0 and 255")
+
+ if value[2] > 255 or value[2] <0:
+ raise ColorValueError("Blue value is not between 0 and 255")
+
+ return
+
+class BooleanConstraintError(Exception):
+ pass
+
+class BooleanConstraint(Constraint):
+ """
+ Validates that the value is either True or False.
+ """
+ def validate(self, value):
+ if value == True or value == False:
+ return
+ raise BooleanConstraintError("Value is not True or False")
+
+class EnumConstraintError(Exception):
+ pass
+
+class EnumConstraint(Constraint):
+ """
+ Validates that the value is part of a set of well-defined values.
+ """
+ def __init__(self, accepted_values):
+ """
+ Creates the constraint and stores the list of accepted values.
+
+ @param correct_values A list that contains all the values that will
+ be declared as satisfying the constraint
+ """
+ self._accepted_values = accepted_values
+
+ def validate(self, value):
+ """
+ Ensures that the value that is passed is part of the list of accepted
+ values.
+ """
+ if not value in self._accepted_values:
+ raise EnumConstraintError("Value is not part of the enumeration")
+ return
+
+class FileConstraintError(Exception):
+ pass
+
+class FileConstraint(Constraint):
+ """
+ Ensures that the string given corresponds to an existing file on disk.
+ """
+ def validate(self, value):
+ # TODO : Decide on the architecture for file retrieval on disk
+ # Relative paths? From where? Support macros?
+ #
+ if not os.path.isfile(value):
+ raise FileConstraintError("Non-existing file : %s"%value)
+ return
+ \ No newline at end of file
diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py
index 901820f..f290f1e 100644
--- a/src/sugar/tutorius/core.py
+++ b/src/sugar/tutorius/core.py
@@ -24,6 +24,7 @@ This module contains the core classes for tutorius
import gtk
import logging
import copy
+import os
from sugar.tutorius.dialog import TutoriusDialog
from sugar.tutorius.gtkutils import find_widget
@@ -36,12 +37,13 @@ class Tutorial (object):
Tutorial Class, used to run through the FSM.
"""
- def __init__(self, name, fsm):
+ def __init__(self, name, fsm,filename= None):
"""
Creates an unattached tutorial.
"""
object.__init__(self)
self.name = name
+ self.activity_init_state_filename = filename
self.state_machine = fsm
self.state_machine.set_tutorial(self)
@@ -64,6 +66,7 @@ class Tutorial (object):
self.activity = activity
ObjectStore().activity = activity
ObjectStore().tutorial = self
+ self._prepare_activity()
self.state_machine.set_state("INIT")
def detach(self):
@@ -97,6 +100,21 @@ class Tutorial (object):
#Swith to the next state pointed by the eventfilter
self.set_state(eventfilter.get_next_state())
+
+ def _prepare_activity(self):
+ """
+ Prepare the activity for the tutorial by loading the saved state and
+ emitting gtk signals
+ """
+ #Load the saved activity if any
+ if self.activity_init_state_filename is not None:
+ #For now the file will be saved in the data folder
+ #of the activity root directory
+ filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\
+ self.activity_init_state_filename
+ if os.path.exists(filename):
+ self.activity.read_file(filename)
+
class State(object):
"""
diff --git a/src/sugar/tutorius/editor.py b/src/sugar/tutorius/editor.py
index 1a1eb61..42cc718 100644
--- a/src/sugar/tutorius/editor.py
+++ b/src/sugar/tutorius/editor.py
@@ -14,17 +14,22 @@
# 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
+""" Tutorial Editor Module
+"""
import gtk
import gobject
-import hippo
-import gconf
+#import hippo
+#import gconf
from gettext import gettext as _
+from sugar.tutorius.gtkutils import register_signals_numbered, get_children
+
class WidgetIdentifier(gtk.Window):
"""
- Tool that allows identifying widgets
+ Tool that allows identifying widgets.
+
"""
__gtype_name__ = 'TutoriusWidgetIdentifier'
@@ -32,7 +37,19 @@ class WidgetIdentifier(gtk.Window):
gtk.Window.__init__(self)
self._activity = activity
- self._handlers = []
+ self._handlers = {}
+ # dict of signals to register on the widgets.
+ # key : signal name
+ # value : initial checkbox status
+ signals = {
+ "focus":True,
+ "button-press-event":True,
+ "enter-notify-event":False,
+ "leave-notify-event":False,
+ "key-press-event":True,
+ "text-selected":True,
+ "clicked":True,
+ }
self.set_decorated(False)
self.set_resizable(False)
@@ -47,63 +64,249 @@ class WidgetIdentifier(gtk.Window):
self._expander.show()
- vbox = gtk.VBox()
- self._expander.add(vbox)
- vbox.show()
-
+ nbk = gtk.Notebook()
+ self._expander.add(nbk)
+ nbk.show()
+ ###############################
+ # Event log viewer page
+ ###############################
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)
+ swd = gtk.ScrolledWindow()
+ swd.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ swd.add(self.logview)
self.logview.show()
- vbox.pack_start(sw)
- sw.show()
+ nbk.append_page(swd, gtk.Label(_("Log")))
+ swd.show()
+
+ ###############################
+ # Filters page
+ ###############################
+ filters = gtk.Table( (len(signals)+1)/2, 2)
+
+ xpos, ypos = 0, 0
+ for key, active in signals.items():
+ cbtn = gtk.CheckButton(label=key)
+ filters.attach(cbtn, xpos, xpos+1, ypos, ypos+1)
+ cbtn.show()
+ cbtn.set_active(active)
+ if active:
+ self._handlers[key] = register_signals_numbered( \
+ self._activity, self._handle_events, events=(key,))
+ else:
+ self._handlers[key] = []
+
+ cbtn.connect("toggled", self.__filter_toggle_cb, key)
+
+ #Follow lines then columns
+ xpos, ypos = (xpos+1)%2, ypos+(xpos%2)
+
+ nbk.append_page(filters, gtk.Label(_("Events")))
+ filters.show()
+
+ ###############################
+ # Explorer Page
+ ###############################
+ tree = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+ explorer = gtk.TreeView(tree)
+
+ pathrendr = gtk.CellRendererText()
+ pathrendr.set_properties(background="#ffffff", foreground="#000000")
+ pathcol = gtk.TreeViewColumn(_("Path"), pathrendr, text=0, background=0, foreground=0)
+ explorer.append_column(pathcol)
+
+ typerendr = gtk.CellRendererText()
+ typerendr.set_properties(background="#ffffff", foreground="#000000")
+ typecol = gtk.TreeViewColumn(_("Widget"), typerendr, text=1, background=1, foreground=1)
+ explorer.append_column(typecol)
+
+ self.__populate_treestore(
+ tree, #tree
+ tree.append(None, ["0",self._activity.get_name()]), #parent
+ self._activity, #widget
+ "0" #path
+ )
+
+ explorer.set_expander_column(typecol)
+
+ swd2 = gtk.ScrolledWindow()
+ swd2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ swd2.add(explorer)
+ explorer.show()
+ nbk.append_page(swd2, gtk.Label(_("Explorer")))
+ swd2.show()
- from sugar.tutorius.gtkutils import register_signals_numbered
- self._handlers = register_signals_numbered(self._activity, self._handle_events)
+ ###############################
+ # GObject Explorer Page
+ ###############################
+ tree2 = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+ explorer2 = gtk.TreeView(tree2)
- def __expander_cb(self, *args):
+ pathrendr2 = gtk.CellRendererText()
+ pathrendr2.set_properties(background="#ffffff", foreground="#000000")
+ pathcol2 = gtk.TreeViewColumn(_("Path"), pathrendr2, text=0, background=0, foreground=0)
+ explorer2.append_column(pathcol2)
+
+ typerendr2 = gtk.CellRendererText()
+ typerendr2.set_properties(background="#ffffff", foreground="#000000")
+ typecol2 = gtk.TreeViewColumn(_("Widget"), typerendr2, text=1, background=1, foreground=1)
+ explorer2.append_column(typecol2)
+
+ self.__populate_gobject_treestore(
+ tree2, #tree
+ tree2.append(None, ["activity",self._activity.get_name()]), #parent
+ self._activity, #widget
+ "activity" #path
+ )
+
+ explorer2.set_expander_column(typecol2)
+
+ swd3 = gtk.ScrolledWindow()
+ swd3.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ swd3.add(explorer2)
+ explorer2.show()
+ nbk.append_page(swd3, gtk.Label(_("GObject Explorer")))
+ swd3.show()
+
+ def __populate_treestore(self, tree, parent, widget, path):
+ """Populates the treestore with the widget's children recursively
+ @param tree gtk.TreeStore to populate
+ @param parent gtk.TreeIter to append to
+ @param widget gtk.Widget to check for children
+ @param path treeish of the widget
+ """
+ #DEBUG: show parameters in log window gehehe
+ #self._handle_events((path,str(type(widget))))
+ children = get_children(widget)
+ for i in xrange(len(children)):
+ childpath = ".".join([path, str(i)])
+ child = children[i]
+ self.__populate_treestore(
+ tree, #tree
+ tree.append(parent, [childpath, child.get_name()]), #parent
+ child, #widget
+ childpath #path
+ )
+
+
+ def __populate_gobject_treestore(self, tree, parent, widget, path, listed=None):
+ """Populates the treestore with the widget's children recursively
+ @param tree gtk.TreeStore to populate
+ @param parent gtk.TreeIter to append to
+ @param widget gtk.Widget to check for children
+ @param path treeish of the widget
+ """
+ listed = listed or []
+ if widget in listed:
+ return
+ listed.append(widget)
+ #DEBUG: show parameters in log window gehehe
+ #self._handle_events((path,str(type(widget))))
+ #Add a child node
+ children = tree.append(parent, ["","children"])
+ for i in dir(widget):
+ #Add if a gobject
+ try:
+ child = getattr(widget, i)
+ except:
+ continue
+ if isinstance(child,gobject.GObject):
+ childpath = ".".join([path, i])
+ child = getattr(widget, i)
+ self.__populate_gobject_treestore(
+ tree, #tree
+ tree.append(children, [childpath, i]), #parent
+ child, #widget
+ path + "." + i, #path,
+ listed
+ )
+ widgets = tree.append(parent, ["","widgets"])
+ wchildren = get_children(widget)
+ for i in xrange(len(wchildren)):
+ childpath = ".".join([path, str(i)])
+ child = wchildren[i]
+ self.__populate_gobject_treestore(
+ tree, #tree
+ tree.append(widgets, [childpath, (hasattr(child,"get_name") and child.get_name()) or i]), #parent
+ child, #widget
+ childpath, #path,
+ listed
+ )
+
+ #Add signals and attributes nodes
+ signals = tree.append(parent, ["","signals"])
+ for signame in gobject.signal_list_names(widget):
+ tree.append(signals, ["",signame])
+
+ attributes = tree.append(parent, ["","properties"])
+ for prop in gobject.list_properties(widget):
+ tree.append(attributes, ["",prop])
+
+ def __filter_toggle_cb(self, btn, eventname):
+ """Callback for signal name checkbuttons' toggling"""
+ #Disconnect existing handlers on key
+ self.__disconnect_handlers(eventname)
+ if btn.get_active():
+ #if checked, reconnect
+ self._handlers[eventname] = register_signals_numbered( \
+ self._activity, self._handle_events, events=(eventname,))
+
+
+ def __expander_cb(self, *args):
+ """Callback for the window expander toggling"""
if self._expander.get_expanded():
self.__move_expanded()
else:
self.__move_collapsed()
def __move_expanded(self):
- width = 400
+ """Move the window to it's expanded position"""
+ width = 500
height = 300
- ww = gtk.gdk.screen_width()
- wh = gtk.gdk.screen_height()
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
self.set_size_request(width, height)
- self.move((ww-width)/2, wh-height)
+ self.move((swidth-width)/2, sheight-height)
def __move_collapsed(self):
+ """Move the window to it's collapsed position"""
width = 150
height = 40
- ww = gtk.gdk.screen_width()
- wh = gtk.gdk.screen_height()
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
self.set_size_request(width, height)
- self.move((ww-width)/2, wh-height)
+ self.move((swidth-width)/2, sheight-height)
def __realize_cb(self, widget):
+ """Callback for realize"""
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 = []
+ """ Disconnect all event handlers """
+ for key in self._handlers:
+ self.__disconnect_handlers(key)
+
+ def __disconnect_handlers(self, key):
+ """ Disconnect event handlers associated to signal name "key" """
+ if self._handlers.has_key(key):
+ for widget, handlerid in self._handlers[key]:
+ widget.handler_disconnect(handlerid)
+ del self._handlers[key]
- def _handle_events(self,*args):
+ def _handle_events(self, *args):
+ """ Event handler for subscribed widget events.
+ Accepts variable length argument list. Last must be
+ a two-tuple containing (event name, widget name) """
sig, name = args[-1]
text = "\r\n".join(
(["%s event received from %s" % (sig, name)] +
diff --git a/src/sugar/tutorius/filters.py b/src/sugar/tutorius/filters.py
index a69055a..594ad6a 100644
--- a/src/sugar/tutorius/filters.py
+++ b/src/sugar/tutorius/filters.py
@@ -16,9 +16,14 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject
-
+import gtk
+import logging
+logger = logging.getLogger("filters")
from sugar.tutorius.gtkutils import find_widget
+from sugar.tutorius.services import ObjectStore
+
+
class EventFilter(object):
"""
Base class for an event filter
@@ -168,4 +173,68 @@ class GtkWidgetEventFilter(EventFilter):
self._widget.handler_disconnect(self._handler_id)
self._handler_id=None
-
+class GtkWidgetTypeFilter(EventFilter):
+ """
+ Event Filter that listens for keystrokes on a widget
+ """
+ def __init__(self, next_state, object_id, text=None, strokes=None):
+ """Constructor
+ @param next_state default EventFilter param, passed on to EventFilter
+ @param object_id object tree-ish identifier
+ @param text resulting text expected
+ @param strokes list of strokes expected
+
+ At least one of text or strokes must be supplied
+ """
+ super(GtkWidgetTypeFilter, self).__init__(next_state)
+ self._object_id = object_id
+ self._text = text
+ self._captext = ""
+ self._strokes = strokes
+ self._capstrokes = []
+ self._widget = None
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install handlers
+ @param callback default EventFilter callback arg
+ """
+ super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs)
+ logger.debug("~~~GtkWidgetTypeFilter install")
+ activity = ObjectStore().activity
+ if activity is None:
+ logger.error("No activity")
+ raise RuntimeWarning("no activity in the objectstore")
+
+ self._widget = find_widget(activity, self._object_id)
+ if self._widget:
+ self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb)
+ logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) )
+
+ def remove_handlers(self):
+ """remove handlers"""
+ super(GtkWidgetTypeFilter, self).remove_handlers()
+ #if an event was connected, disconnect it
+ if self._handler_id:
+ self._widget.handler_disconnect(self._handler_id)
+ self._handler_id=None
+
+ def __keypress_cb(self, widget, event, *args):
+ """keypress callback"""
+ logger.debug("~~~keypressed!")
+ key = event.keyval
+ keystr = event.string
+ logger.debug("~~~Got key: " + str(key) + ":"+ keystr)
+ self._capstrokes += [key]
+ #TODO Treat other stuff, such as arrows
+ if key == gtk.keysyms.BackSpace:
+ self._captext = self._captext[:-1]
+ else:
+ self._captext = self._captext + keystr
+
+ logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext))
+ if not self._strokes is None and self._strokes in self._capstrokes:
+ self.do_callback()
+ if not self._text is None and self._text in self._captext:
+ self.do_callback()
+
diff --git a/src/sugar/tutorius/gtkutils.py b/src/sugar/tutorius/gtkutils.py
index 1870dc4..a745b9d 100644
--- a/src/sugar/tutorius/gtkutils.py
+++ b/src/sugar/tutorius/gtkutils.py
@@ -19,11 +19,6 @@ Utility classes and functions that are gtk related
"""
import gtk
-def activity(activity=None, singleton=[]):
- if activity:
- singleton.append(activity)
- return singleton[0]
-
def find_widget(base, target_fqdn):
"""Find a widget by digging into a parent widget's children tree
@@ -47,13 +42,13 @@ def find_widget(base, target_fqdn):
while len(path) > 0:
try:
- obj = obj.get_children()[int(path.pop(0))]
+ obj = get_children(obj)[int(path.pop(0))]
except:
break
return obj
-EVENTS = [
+EVENTS = (
"focus",
"button-press-event",
"enter-notify-event",
@@ -61,9 +56,9 @@ EVENTS = [
"key-press-event",
"text-selected",
"clicked",
-]
+)
-IGNORED_WIDGETS = [
+IGNORED_WIDGETS = (
"GtkVBox",
"GtkHBox",
"GtkAlignment",
@@ -71,9 +66,9 @@ IGNORED_WIDGETS = [
"GtkButton",
"GtkToolItem",
"GtkToolbar",
-]
+)
-def register_signals_numbered(target, handler, prefix="0", max_depth=None):
+def register_signals_numbered(target, handler, prefix="0", max_depth=None, events=None):
"""
Recursive function to register event handlers on an target
and it's children. The event handler is called with an extra
@@ -96,25 +91,24 @@ def register_signals_numbered(target, handler, prefix="0", max_depth=None):
@returns list of (object, handler_id)
"""
ret = []
+ evts = events or EVENTS
#Gtk Containers have a get_children() function
- if hasattr(target, "get_children") and \
- hasattr(target.get_children, "__call__"):
- children = target.get_children()
- for i in range(len(children)):
- child = children[i]
- if max_depth is None or max_depth > 0:
- #Recurse with a prefix on all children
- pre = ".".join( \
- [p for p in (prefix, str(i)) if not p is None]
- )
- if max_depth is None:
- dep = None
- else:
- dep = max_depth - 1
- ret+=register_signals_numbered(child, handler, pre, dep)
+ children = get_children(target)
+ for i in range(len(children)):
+ child = children[i]
+ if max_depth is None or max_depth > 0:
+ #Recurse with a prefix on all children
+ pre = ".".join( \
+ [p for p in (prefix, str(i)) if not p is None]
+ )
+ if max_depth is None:
+ dep = None
+ else:
+ dep = max_depth - 1
+ ret+=register_signals_numbered(child, handler, pre, dep, evts)
#register events on the target if a widget XXX necessary to check this?
if isinstance(target, gtk.Widget):
- for sig in EVENTS:
+ for sig in evts:
try:
ret.append( \
(target, target.connect(sig, handler, (sig, prefix) ))\
@@ -124,7 +118,7 @@ def register_signals_numbered(target, handler, prefix="0", max_depth=None):
return ret
-def register_signals(target, handler, prefix=None, max_depth=None):
+def register_signals(target, handler, prefix=None, max_depth=None, events=None):
"""
Recursive function to register event handlers on an target
and it's children. The event handler is called with an extra
@@ -148,28 +142,27 @@ def register_signals(target, handler, prefix=None, max_depth=None):
@returns list of (object, handler_id)
"""
ret = []
+ evts = events or EVENTS
#Gtk Containers have a get_children() function
- if hasattr(target, "get_children") and \
- hasattr(target.get_children, "__call__"):
- for child in target.get_children():
- if max_depth is None or max_depth > 0:
- #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 IGNORED_WIDGETS)] \
- )
- if max_depth is None:
- dep = None
- else:
- dep = max_depth - 1
- ret += register_signals(child, handler, pre, dep)
+ for child in get_children(target):
+ if max_depth is None or max_depth > 0:
+ #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 IGNORED_WIDGETS)] \
+ )
+ if max_depth is None:
+ dep = None
+ else:
+ dep = max_depth - 1
+ ret += register_signals(child, handler, pre, dep, evts)
name = ".".join( \
[p for p in (prefix, target.get_name()) \
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 EVENTS:
+ for sig in evts:
try:
ret.append( \
(target, target.connect(sig, handler, (sig, name) )) \
@@ -179,3 +172,19 @@ def register_signals(target, handler, prefix=None, max_depth=None):
return ret
+def get_children(widget):
+ """Lists widget's children"""
+ #widgets with multiple children
+ try:
+ return widget.get_children()
+ except (AttributeError,TypeError):
+ pass
+
+ #widgets with a single child
+ try:
+ return [widget.get_child(),]
+ except (AttributeError,TypeError):
+ pass
+
+ #otherwise return empty list
+ return []
diff --git a/src/sugar/tutorius/properties.py b/src/sugar/tutorius/properties.py
new file mode 100644
index 0000000..5be7e1c
--- /dev/null
+++ b/src/sugar/tutorius/properties.py
@@ -0,0 +1,204 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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
+
+from sugar.tutorius.constraints import *
+
+class TutoriusProperty():
+ """
+ The base class for all actions' properties. The interface is the following :
+
+ set() : attempts to change the value (may throw an exception if constraints
+ are not respected
+
+ value : the value of the property
+
+ type : the type of the property
+
+ get_contraints() : the constraints inserted on this property. They define
+ what is acceptable or not as values.
+ """
+ def __init__(self):
+ self._type = None
+ self._constraints = None
+ self._value = None
+
+ def set(self, value):
+ """
+ Attempts to set the value of the property. If the value does not respect
+ the constraints on the property, this method will raise an exception.
+
+ The exception should be of the type related to the constraint that
+ failed. E.g. When a int is to be set with a value that
+ """
+ for constraint_name in self.get_constraints():
+ constraint = getattr(self, constraint_name)
+ constraint.validate(value)
+ self._value = value
+ return True
+
+ def get(self):
+ return self._value
+
+ value = property(fget=get)
+
+ def get_constraints(self):
+ """
+ Returns the list of constraints associated to this property.
+ """
+ if self._constraints is None:
+ self._constraints = []
+ for i in dir(self):
+ t = getattr(self,i)
+ if isinstance(t, Constraint):
+ self._constraints.append(i)
+ return self._constraints
+
+ def get_type(self):
+ return self._type
+
+ type = property(fget=get_type)
+
+class TIntProperty(TutoriusProperty):
+ """
+ Represents an integer. Can have an upper value limit and/or a lower value
+ limit.
+ """
+
+ def __init__(self, value, lower_limit=None, upper_limit=None):
+ TutoriusProperty.__init__(self)
+ self._type = "int"
+ self.upper_limit = UpperLimitConstraint(upper_limit)
+ self.lower_limit = LowerLimitConstraint(lower_limit)
+
+ self.set(value)
+
+class TFloatProperty(TutoriusProperty):
+ """
+ Represents a floting point number. Can have an upper value limit and/or
+ a lower value limit.
+ """
+ def __init__(self, value, lower_limit=None, upper_limit=None):
+ TutoriusProperty.__init__(self)
+ self._type = "float"
+
+ self.upper_limit = UpperLimitConstraint(upper_limit)
+ self.lower_limit = LowerLimitConstraint(lower_limit)
+
+ self.set(value)
+
+class TStringProperty(TutoriusProperty):
+ """
+ Represents a string. Can have a maximum size limit.
+ """
+ def __init__(self, value, size_limit=None):
+ TutoriusProperty.__init__(self)
+ self._type = "string"
+ self.size_limit = SizeConstraint(size_limit)
+
+ self.set(value)
+
+class TArrayProperty(TutoriusProperty):
+ """
+ Represents an array of properties. Can have a maximum number of element
+ limit, but there are no constraints on the content of the array.
+ """
+ def __init__(self, value, size_limit=None):
+ TutoriusProperty.__init__(self)
+ self._type = "array"
+ self.size_limit = SizeConstraint(size_limit)
+
+ self.set(value)
+
+class TColorProperty(TutoriusProperty):
+ """
+ Represents a RGB color with 3 8-bit integer values.
+
+ The value of the property is the array [R, G, B]
+ """
+ def __init__(self, red=None, green=None, blue=None):
+ TutoriusProperty.__init__(self)
+ self._type = "color"
+
+ self.color_constraint = ColorConstraint()
+
+ self._red = red or 0
+ self._green = green or 0
+ self._blue = blue or 0
+
+ self.set([self._red, self._green, self._blue])
+
+class TFileProperty(TutoriusProperty):
+ """
+ Represents a path to a file on the disk.
+ """
+ def __init__(self, path):
+ """
+ Defines the path to an existing file on disk file.
+
+ For now, the path may be relative or absolute, as long as it exists on
+ the local machine.
+ TODO : Make sure that we have a file scheme that supports distribution
+ on other computers (LP 355197)
+ """
+ TutoriusProperty.__init__(self)
+
+ self._type = "file"
+
+ self.file_constraint = FileConstraint()
+
+ self.set(path)
+
+class TEnumProperty(TutoriusProperty):
+ """
+ Represents a value in a given enumeration. This means that the value will
+ always be one in the enumeration and nothing else.
+
+ """
+ def __init__(self, value, accepted_values):
+ """
+ Creates the enumeration property.
+
+ @param value The initial value of the enum. Must be part of
+ accepted_values
+ @param accepted_values A list of values that the property can take
+ """
+ TutoriusProperty.__init__(self)
+
+ self._type = "enum"
+
+ self.enum_constraint = EnumConstraint(accepted_values)
+
+ self.set(value)
+
+class TBooleanProperty(TutoriusProperty):
+ """
+ Represents a True of False value.
+ """
+ def __init__(self, value=False):
+ TutoriusProperty.__init__(self)
+
+ self._type = "boolean"
+
+ self.boolean_constraint = BooleanConstraint()
+
+ self.set(value)
+
+class TUAMProperty(TutoriusProperty):
+ """
+ Represents a widget of the interface by storing its UAM.
+ """
+ # TODO : Pending UAM check-in (LP 355199)
+ pass
diff --git a/src/sugar/tutorius/tests/actiontests.py b/src/sugar/tutorius/tests/actiontests.py
new file mode 100644
index 0000000..ab9cdba
--- /dev/null
+++ b/src/sugar/tutorius/tests/actiontests.py
@@ -0,0 +1,173 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@gmail.com>
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@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
+"""
+Action tests
+
+The behavior of the actions must be tested here.
+"""
+
+import unittest
+import gtk
+
+from sugar.tutorius.actions import *
+from sugar.tutorius.services import ObjectStore
+
+class PropertyAction(Action):
+ def __init__(self, na):
+ self._a = na
+
+ def set_a(self, na):
+ self._a = na
+
+ def get_a(self):
+ return self._a
+
+ a = property(fget=get_a, fset=set_a)
+
+class PropsTest(unittest.TestCase):
+
+ def test_get_properties(self):
+ prop = PropertyAction(8)
+
+ assert prop.get_properties() == ['a'], "Action does not contain property 'a'"
+
+class CountAction(Action):
+ """
+ This action counts how many times it's do and undo methods get called
+ """
+ def __init__(self):
+ self.do_count = 0
+ self.undo_count = 0
+
+ def do(self):
+ self.do_count += 1
+
+ def undo(self):
+ self.undo_count += 1
+
+
+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):
+ """
+ Validate that the OnceWrapper wrapper works properly using the
+ CountAction
+ """
+ act = CountAction()
+ wrap = OnceWrapper(act)
+
+ assert act.do_count == 0, "do() should not have been called in __init__()"
+ assert act.undo_count == 0, "undo() should not have been called in __init__()"
+
+ wrap.undo()
+
+ assert act.undo_count == 0, "undo() should not be called if do() has not been called"
+
+ wrap.do()
+ assert act.do_count == 1, "do() should have been called once"
+
+ wrap.do()
+ assert act.do_count == 1, "do() should have been called only once"
+
+ wrap.undo()
+ assert act.undo_count == 1, "undo() should have been called once"
+
+ wrap.undo()
+ assert act.undo_count == 1, "undo() should have been called only once"
+
+class ChainTester(Action):
+ def __init__(self, witness):
+ self._witness = witness
+
+ def do(self, **kwargs):
+ self._witness.append([self,"do"])
+
+ def undo(self):
+ self._witness.append([self,"undo"])
+
+class ChainActionTest(unittest.TestCase):
+ """Tester for ChainAction"""
+ def test_empty(self):
+ """If the expected empty behavior (do nothing) changes
+ and starts throwing exceptions, this will flag it"""
+ a = ChainAction()
+ a.do()
+ a.undo()
+
+ def test_order(self):
+ witness = []
+ first = ChainTester(witness)
+ second = ChainTester(witness)
+
+ c = ChainAction(first, second)
+ assert witness == [], "Actions should not be triggered on init"""
+ c.do()
+
+ assert witness[0][0] is first, "First triggered action must be 'first'"
+ assert witness[0][1] is "do", "Action do() should be triggered"
+
+ assert witness[1][0] is second, "second triggered action must be 'second'"
+ assert witness[1][1] is "do", "Action do() should be triggered"
+
+ assert len(witness) is 2, "Two actions should give 2 do's"
+
+ #empty the witness list
+ while len(witness):
+ rm = witness.pop()
+
+ c.undo()
+ assert witness[1][0] is first, "second triggered action must be 'first'"
+ assert witness[1][1] is "undo", "Action undo() should be triggered"
+
+ assert witness[0][0] is second, "first triggered action must be 'second'"
+ assert witness[0][1] is "undo", "Action undo() should be triggered"
+
+ assert len(witness) is 2, "Two actions should give 2 undo's"
+
+class DisableWidgetActionTests(unittest.TestCase):
+ def test_disable(self):
+ btn = gtk.Button()
+ ObjectStore().activity = btn
+ btn.set_sensitive(True)
+
+ assert btn.props.sensitive is True, "Callback should have been called"
+
+ act = DisableWidgetAction("0")
+ assert btn.props.sensitive is True, "Callback should have been called again"
+ act.do()
+ assert btn.props.sensitive is False, "Callback should not have been called again"
+ act.undo()
+ assert btn.props.sensitive is True, "Callback should have been called again"
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/src/sugar/tutorius/tests/constraintstests.py b/src/sugar/tutorius/tests/constraintstests.py
new file mode 100644
index 0000000..407cc24
--- /dev/null
+++ b/src/sugar/tutorius/tests/constraintstests.py
@@ -0,0 +1,211 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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 unittest
+
+from sugar.tutorius.constraints import *
+
+class ConstraintTest(unittest.TestCase):
+ def test_base_class(self):
+ cons = Constraint()
+ try:
+ cons.validate(1)
+ assert False, "Base class should throw an assertion"
+ except NotImplementedError:
+ pass
+
+class ValueConstraintTest(unittest.TestCase):
+ def test_limit_set(self):
+ cons = ValueConstraint(12)
+
+ assert cons.limit == 12
+
+class UpperLimitConstraintTest(unittest.TestCase):
+ def test_empty_constraint(self):
+ cons = UpperLimitConstraint(None)
+ try:
+ cons.validate(20)
+ except UpperLimitConstraintError:
+ assert False, "Empty contraint should not raise an exception"
+
+ def test_validate(self):
+ cons = UpperLimitConstraint(10)
+
+ try:
+ cons.validate(20)
+ assert False, "Validation of UpperLimit(10) on 20 should raise an exception"
+ except UpperLimitConstraintError:
+ pass
+
+ try:
+ cons.validate(5)
+ except UpperLimitConstraintError:
+ assert True, "Validation of UpperLimit(10) on 5 should not raise an exception"
+
+class LowerLimitConstraintTest(unittest.TestCase):
+ def test_empty_constraint(self):
+ cons = LowerLimitConstraint(None)
+ try:
+ cons.validate(20)
+ except LowerLimitConstraintError:
+ assert False, "Empty contraint should not raise an exception"
+
+ def test_validate(self):
+ cons = LowerLimitConstraint(10)
+
+ try:
+ cons.validate(5)
+ assert False, "Validation of LowerLimit(10) on 5 should raise an exception"
+ except LowerLimitConstraintError:
+ pass
+
+ try:
+ cons.validate(20)
+ except LowerLimitConstraintError:
+ assert True, "Validation of LowerLimit(10) on 20 should not raise an exception"
+
+class SizeConstraintTest(unittest.TestCase):
+ def test_empty_constraint(self):
+ cons = SizeConstraint(None)
+ try:
+ cons.validate(20)
+ except SizeConstraintError:
+ assert False, "Empty contraint should not raise an exception"
+
+ def test_validate(self):
+ cons = SizeConstraint(10)
+
+ try:
+ cons.validate(range(0, 20))
+ assert False, "Validation of SizeLimit(10) on list of length 20 should raise an exception"
+ except SizeConstraintError:
+ pass
+
+ try:
+ cons.validate(range(0,5))
+ except SizeConstraintError:
+ assert True, "Validation of SizeLimit(10) on list of length 5 should not raise an exception"
+
+class ColorConstraintTest(unittest.TestCase):
+ def test_validate(self):
+ cons = ColorConstraint()
+
+ try:
+ cons.validate([0, 0])
+ assert False, "ColorConstraint on list of length 2 should raise an exception"
+ except ColorArraySizeError:
+ pass
+ except ColorConstraintError:
+ assert False, "ColorConstraint threw the wrong type of error"
+
+ try:
+ cons.validate([0, 0, "str"])
+ assert False, "ColorConstraint on with non-integers values should raise an exception"
+ except ColorTypeError:
+ pass
+ except ColorConstraintError:
+ assert False, "ColorConstraint threw the wrong type of error"
+
+ try:
+ cons.validate([0, "str", 0])
+ assert False, "ColorConstraint on with non-integers values should raise an exception"
+ except ColorTypeError:
+ pass
+ except ColorConstraintError:
+ assert False, "ColorConstraint threw the wrong type of error"
+
+ try:
+ cons.validate(["str", 0, 0])
+ assert False, "ColorConstraint on with non-integers values should raise an exception"
+ except ColorTypeError:
+ pass
+ except ColorConstraintError:
+ assert False, "ColorConstraint threw the wrong type of error"
+
+ try:
+ cons.validate([1, 2, 300])
+ assert False, "ColorConstraint on with non-integers values should raise an exception"
+ except ColorValueError:
+ pass
+ except ColorConstraintError:
+ assert False, "ColorConstraint threw the wrong type of error"
+
+ try:
+ cons.validate([1, -100, 30])
+ assert False, "ColorConstraint on with non-integers values should raise an exception"
+ except ColorValueError:
+ pass
+ except ColorConstraintError:
+ assert False, "ColorConstraint threw the wrong type of error"
+
+ try:
+ cons.validate([999999, 2, 300])
+ assert False, "ColorConstraint on with non-integers values should raise an exception"
+ except ColorValueError:
+ pass
+ except ColorConstraintError:
+ assert False, "ColorConstraint threw the wrong type of error"
+
+ try:
+ cons.validate([12, 23, 34])
+ except LowerLimitConstraintError:
+ assert True, "ColorConstraint on expected input should not raise an exception"
+
+class BooleanConstraintTest(unittest.TestCase):
+ def test_validate(self):
+ cons = BooleanConstraint()
+
+ cons.validate(True)
+ cons.validate(False)
+
+ try:
+ cons.validate(18)
+ assert False, "Setting integer on constraint should raise an error"
+ except BooleanConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type raised when setting wrong type"
+
+class EnumConstraintTest(unittest.TestCase):
+ def test_validate(self):
+ cons = EnumConstraint([1,2,3,7,8,9, "ex"])
+
+ cons.validate(8)
+
+ cons.validate("ex")
+
+ try:
+ cons.validate(4)
+ assert False, "There should be an exception on setting a value out of the enum"
+ except EnumConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type thrown"
+
+class FileConstraintTest(unittest.TestCase):
+ def test_validate(self):
+ cons = FileConstraint()
+
+ cons.validate("run-tests.py")
+
+ try:
+ cons.validate("unknown/file.py")
+ assert False, "Non-existing file check should throw an exception"
+ except FileConstraintError:
+ pass
+
+if __name__ == "__main__":
+ unittest.main() \ No newline at end of file
diff --git a/src/sugar/tutorius/tests/coretests.py b/src/sugar/tutorius/tests/coretests.py
index ec730f2..5f91a64 100644
--- a/src/sugar/tutorius/tests/coretests.py
+++ b/src/sugar/tutorius/tests/coretests.py
@@ -18,18 +18,24 @@
Core Tests
This module contains all the tests that pertain to the usage of the Tutorius
-Core. This means that the Event Filters, the Finite State Machine and all the
+Core. This means that the the Finite State Machine, States and all the
related elements and interfaces are tested here.
+Usage of actions and event filters is tested, but not the concrete actions
+and event filters. Those are in their separate test module
+
"""
import unittest
import logging
-from sugar.tutorius.actions import Action, OnceWrapper
+from sugar.tutorius.actions import Action, ClickAction, TypeTextAction
from sugar.tutorius.core import *
from sugar.tutorius.filters import *
+
+from actiontests import CountAction
+
# Helper classes to help testing
class SimpleTutorial(Tutorial):
"""
@@ -58,21 +64,46 @@ class TrueWhileActiveAction(Action):
def undo(self):
self.active = False
+
+class ClickableWidget():
+ """
+ This class fakes a widget with a clicked() method
+ """
+ def __init__(self):
+ self.click_count = 0
+
+ def clicked(self):
+ self.click_count += 1
+
+class FakeTextEntry():
+ """
+ This class fakes a widget with an insert_text() method
+ """
+ def __init__(self):
+ self.text_lines = []
+ self.last_entered_line = ""
+ self.displayed_text = ""
+
+ def insert_text(self, text, index):
+ self.last_entered_line = text
+ self.text_lines.append(text)
+ self.displayed_text = self.displayed_text[0:index] + text + self.displayed_text[index+1:]
-
-class CountAction(Action):
+class FakeParentWidget():
"""
- This action counts how many times it's do and undo methods get called
+ This class fakes a widet container, it implements the get_children() method
"""
def __init__(self):
- self.do_count = 0
- self.undo_count = 0
+ self._children = []
+
+ def add_child(self, child):
+ self._children.append(child)
+
+ def get_children(self):
+ return self._children
+
- def do(self):
- self.do_count += 1
- def undo(self):
- self.undo_count += 1
class TriggerEventFilter(EventFilter):
"""
@@ -110,49 +141,96 @@ class FakeEventFilter(TriggerEventFilter):
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"
+class ClickActionTests(unittest.TestCase):
+ """
+ Test class for click action
+ """
+ def test_do_action(self):
+ activity = FakeParentWidget()
+ widget = ClickableWidget()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
+
+ action = ClickAction("0.0")
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.do()
+
+ assert widget.click_count == 1, "clicked() should have been called by do()"
+
+ action.do()
+
+ assert widget.click_count == 2, "clicked() should have been called by do()"
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):
- """
- Validate that the OnceWrapper wrapper works properly using the
- CountAction
- """
- act = CountAction()
- wrap = OnceWrapper(act)
-
- assert act.do_count == 0, "do() should not have been called in __init__()"
- assert act.undo_count == 0, "undo() should not have been called in __init__()"
-
- wrap.undo()
-
- assert act.undo_count == 0, "undo() should not be called if do() has not been called"
-
- wrap.do()
- assert act.do_count == 1, "do() should have been called once"
+ activity = FakeParentWidget()
+ widget = ClickableWidget()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
+
+ action = ClickAction("0.0")
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.undo()
+
+ #There is no undo for this action so the test should not fail
+ assert True
+
+
- wrap.do()
- assert act.do_count == 1, "do() should have been called only once"
+class TypeTextActionTests(unittest.TestCase):
+ """
+ Test class for type text action
+ """
+ def test_do_action(self):
+ activity = FakeParentWidget()
+ widget = FakeTextEntry()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
- wrap.undo()
- assert act.undo_count == 1, "undo() should have been called once"
+ test_text = "This is text"
+
+
+ action = TypeTextAction("0.0", test_text)
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.do()
+
+ assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
+
+ action.do()
+
+ assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
+ assert len(widget.text_lines) == 2, "insert_text() should have been called twice"
- wrap.undo()
- assert act.undo_count == 1, "undo() should have been called only once"
+ def test_undo(self):
+ activity = FakeParentWidget()
+ widget = FakeTextEntry()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
+ test_text = "This is text"
+
+
+ action = TypeTextAction("0.0", test_text)
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.undo()
+
+ #There is no undo for this action so the test should not fail
+ assert True
# State testing class
class StateTest(unittest.TestCase):
diff --git a/src/sugar/tutorius/tests/filterstests.py b/src/sugar/tutorius/tests/filterstests.py
new file mode 100644
index 0000000..8ee6cc8
--- /dev/null
+++ b/src/sugar/tutorius/tests/filterstests.py
@@ -0,0 +1,200 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@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
+"""
+Filters Tests
+
+This module contains all the tests that pertain to the usage of the Tutorius
+Event Filters
+"""
+
+import unittest
+import time
+import gobject
+import gtk
+
+from sugar.tutorius.filters import EventFilter, TimerEvent, GtkWidgetEventFilter, GtkWidgetTypeFilter
+from gtkutilstests import SignalCatcher
+
+class BaseEventFilterTests(unittest.TestCase):
+ """Test the behavior of the Base EventFilter class"""
+ def test_properties(self):
+ """Test EventFilter properties"""
+ e = EventFilter("NEXTSTATE")
+
+ assert e.next_state == "NEXTSTATE", "next_state should have value used in constructor"
+
+ e.next_state = "NEWSTATE"
+
+ assert e.next_state == "NEWSTATE", "next_state should have been changed by setter"
+
+
+ def test_callback(self):
+ """Test the callback mechanism"""
+ e = EventFilter("Next")
+ s = SignalCatcher()
+
+ #Trigger the do_callback, shouldn't do anything
+ e.do_callback()
+
+ #Install the handler
+ e.install_handlers(s.callback)
+
+ #Trigger the do_callback, s should receive e
+ e.do_callback()
+ assert s.data[0] is e
+
+ s.data = None
+
+ e.remove_handlers()
+
+ #Trigger callback, nothing should happen again
+ e.do_callback()
+
+ assert s.data is None
+
+
+
+
+
+class TestTimerEvent(unittest.TestCase):
+ """Tests for timer"""
+ def test_timer(self):
+ """Make sure timer gets called once, and only once"""
+ gobject.threads_init()
+ ctx = gobject.MainContext()
+ main = gobject.MainLoop(ctx)
+
+ e = TimerEvent("Next",1) #1 second should be enough :s
+ s = SignalCatcher()
+
+ e.install_handlers(s.callback)
+
+ assert s.data is None, "Callback should not have been called yet"
+
+ #process events
+ while gtk.events_pending():
+ gtk.main_iteration(block=False)
+ while ctx.pending():
+ ctx.iteration(may_block=False)
+
+ #Wait 1.4 sec
+ time.sleep(1.4)
+
+ #process events
+ while gtk.events_pending():
+ gtk.main_iteration(block=False)
+ while ctx.pending():
+ ctx.iteration(may_block=False)
+
+ assert not s.data is None, "Callback should have been called"
+
+ s.data = None
+
+ #Wait 1.4 sec
+ time.sleep(1.4)
+
+ #process events
+ while gtk.events_pending():
+ gtk.main_iteration(block=False)
+ while ctx.pending():
+ ctx.iteration(may_block=False)
+
+ assert s.data is None, "Callback should not have been called again"
+
+ def test_timer_stop(self):
+ """Make sure timer can be stopped"""
+ gobject.threads_init()
+ ctx = gobject.MainContext()
+ main = gobject.MainLoop(ctx)
+
+ e = TimerEvent("Next",1) #1 second should be enough :s
+ s = SignalCatcher()
+
+ e.install_handlers(s.callback)
+
+ assert s.data is None, "Callback should not have been called yet"
+
+ #process events
+ while gtk.events_pending():
+ gtk.main_iteration(block=False)
+ while ctx.pending():
+ ctx.iteration(may_block=False)
+
+ assert s.data is None, "Callback should not have been called yet"
+
+ #Wait 0.5 sec
+ time.sleep(0.5)
+
+ e.remove_handlers()
+
+ #Wait 0.5 sec
+ time.sleep(0.7)
+
+ #process events
+ while gtk.events_pending():
+ gtk.main_iteration(block=False)
+ while ctx.pending():
+ ctx.iteration(may_block=False)
+
+ assert s.data is None, "Callback should not have been called"
+
+ s.data = None
+
+
+class TestGtkWidgetEventFilter(unittest.TestCase):
+ """Tests for GtkWidgetEventFilter"""
+ def __init__(self,*args):
+ unittest.TestCase.__init__(self,*args)
+ self.top=None
+ self.btn1=None
+
+ def setUp(self):
+ self.top = gtk.Window()
+ self.btn1 = gtk.Button()
+ self.top.add(self.btn1)
+
+ def test_install(self):
+ h = GtkWidgetEventFilter("Next","0","whatever")
+ try:
+ h.install_handlers(None)
+
+ assert False, "Install handlers should have failed"
+ except TypeError:
+ assert True, "Install should have failed"
+
+ def test_button_clicks(self):
+ h = GtkWidgetEventFilter("Next","0.0","clicked")
+ s = SignalCatcher()
+
+ h.install_handlers(s.callback, activity=self.top)
+
+ assert s.data is None, "no callback to call yet"
+
+ self.btn1.clicked()
+ assert not s.data is None, "callback should have been called"
+ s.data = None
+
+ h.remove_handlers()
+
+ assert s.data is None, "callback must not be called again"
+
+ self.btn1.clicked()
+
+ assert s.data is None, "callback must not be called again"
+
+
+
diff --git a/src/sugar/tutorius/tests/gtkutilstests.py b/src/sugar/tutorius/tests/gtkutilstests.py
index fb9a20b..41634ae 100644
--- a/src/sugar/tutorius/tests/gtkutilstests.py
+++ b/src/sugar/tutorius/tests/gtkutilstests.py
@@ -26,16 +26,22 @@ import unittest
import logging
import gtk, gobject
-from sugar.tutorius.gtkutils import find_widget, register_signals_numbered, register_signals
+from sugar.tutorius.gtkutils import find_widget, register_signals_numbered, register_signals, get_children
class SignalCatcher(object):
+ """Test class that store arguments received on it's callback method.
+ Useful for testing callbacks"""
def __init__(self):
- self.data = []
+ """Constructor"""
+ self.data = None
def callback(self, *args):
+ """Callback function, stores argument list in self.data"""
self.data = args
def disconnect_handlers(hlist):
+ """Disconnect handles in handler list. hlist must be a list of
+ two-tuples (widget, handler_id)"""
for widget, handler in hlist:
try:
widget.handler_disconnect(handler)
@@ -97,6 +103,8 @@ class GtkUtilsTests(unittest.TestCase):
def test_named(self):
#def register_signals(target, handler, prefix=None, max_depth=None):
s=SignalCatcher()
+
+ #Test 0 depth
handler_list = register_signals(self.top, s.callback, max_depth=0)
#remove duplicates in widget list
@@ -104,12 +112,36 @@ class GtkUtilsTests(unittest.TestCase):
assert len(widget_list) == 1, "register_signals should not have recursed (%d objects registered)" % len(widget_list)
- while gtk.events_pending():
- gtk.main_iteration(block=False)
+ assert widget_list[0] == self.top, "register_signals should have gotten only the top"
+
+ disconnect_handlers(handler_list)
+
+ #Test 2 depth
+ handler_list = register_signals(self.top, s.callback, max_depth=2)
+
+ #remove duplicates in widget list
+ widget_list = dict.fromkeys([w for w, h in handler_list]).keys()
+
+ assert len(widget_list) == 5, "expected %d objects (got %d)" % (len(widget_list), 5)
+
+ disconnect_handlers(handler_list)
+
+ #Test Infinite depth
+ handler_list = register_signals(self.top, s.callback, max_depth=None)
+
+ #remove duplicates in widget list
+ widget_list = dict.fromkeys([w for w, h in handler_list]).keys()
+
+ assert len(widget_list) == 7, "expected %d objects (got %d)" % (len(widget_list), 7)
+
+ disconnect_handlers(handler_list)
+
def test_numbered(self):
s=SignalCatcher()
#def register_signals_numbered(target, handler, prefix="0", max_depth=None):
+
+ #Test 0 depth
handler_list = register_signals_numbered(self.top, s.callback, max_depth=0)
#remove duplicates in widget list
@@ -117,15 +149,62 @@ class GtkUtilsTests(unittest.TestCase):
assert len(widget_list) == 1, "register_signals should not have recursed (%d objects registered)" % len(widget_list)
- while gtk.events_pending():
- gtk.main_iteration(block=False)
+ assert widget_list[0] == self.top, "register_signals should have gotten only the top"
+
+ disconnect_handlers(handler_list)
+
+ #Test 1 depth
+ handler_list = register_signals_numbered(self.top, s.callback, max_depth=1)
+
+ #remove duplicates in widget list
+ widget_list = dict.fromkeys([w for w, h in handler_list]).keys()
+
+ assert len(widget_list) == 2, "expected %d objects (got %d)" % (len(widget_list), 2)
+
+ disconnect_handlers(handler_list)
+
+ #Test Infinite depth
+ handler_list = register_signals_numbered(self.top, s.callback, max_depth=None)
+
+ #remove duplicates in widget list
+ widget_list = dict.fromkeys([w for w, h in handler_list]).keys()
+
+ assert len(widget_list) == 7, "expected %d objects (got %d)" % (len(widget_list), 7)
+
+ disconnect_handlers(handler_list)
+
def test_find_widget(self):
+ #Test individual values in the defined widgets
for widget in self.widgets.values():
f = find_widget(self.top, widget["numbered"])
assert f is widget["widget"], "Widget %s found with path %s, expected %s" % (f, widget["numbered"], widget["widget"])
+ #Test out of index
+ f = find_widget(self.top, "0.99.1.2")
+ assert f is self.top, "Should have returned top widget"
+
+ def test_register_args_numbered(self):
+ #Need to check the signal catcher and stuff... grreat
+ while gtk.events_pending():
+ gtk.main_iteration(block=False)
+
+
+ def test_register_args_normal(self):
+ #Need to check the signal catcher and stuff... grreat
+ while gtk.events_pending():
+ gtk.main_iteration(block=False)
+
+ def test_notwidget(self):
+ """Test the get_children function"""
+ o = object()
+ res = get_children(o)
+
+ assert len(res) == 0, "object has no children"
+ top_children = get_children(self.top)
+ expected = [self.widgets["hbox0"]["widget"],]
+ assert top_children == expected, "expected %s for top's children, got %s" % (str(expected),str(top_children))
if __name__ == "__main__":
unittest.main()
diff --git a/src/sugar/tutorius/tests/linear_creatortests.py b/src/sugar/tutorius/tests/linear_creatortests.py
index 3bc06f9..f9ffbe7 100644
--- a/src/sugar/tutorius/tests/linear_creatortests.py
+++ b/src/sugar/tutorius/tests/linear_creatortests.py
@@ -19,8 +19,8 @@ from sugar.tutorius.core import *
from sugar.tutorius.actions import *
from sugar.tutorius.filters import *
from sugar.tutorius.linear_creator import *
-from coretests import TriggerEventFilter, CountAction
-
+from coretests import TriggerEventFilter
+from actiontests import CountAction
import unittest
class CreatorTests(unittest.TestCase):
@@ -64,6 +64,16 @@ class CreatorTests(unittest.TestCase):
assert len(state2.get_action_list()) == 0, "Creator inserted extra actions on wrong state"
assert len(state2.get_event_filter_list()) == 0, "Creator assigner events to the final state"
+
+ creator.action(CountAction())
+
+ fsm = creator.generate_fsm()
+
+ state2 = fsm.get_state_by_name("State2")
+
+ assert len(state2.get_action_list()) == 1, "Creator did not add the action"
+
+ assert len(state2.get_event_filter_list()) == 0, "Creator assigner events to the final state"
if __name__ == '__main__':
- unittest.main() \ No newline at end of file
+ unittest.main()
diff --git a/src/sugar/tutorius/tests/propertiestests.py b/src/sugar/tutorius/tests/propertiestests.py
new file mode 100644
index 0000000..52a9a75
--- /dev/null
+++ b/src/sugar/tutorius/tests/propertiestests.py
@@ -0,0 +1,348 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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 unittest
+
+from sugar.tutorius.constraints import *
+from sugar.tutorius.properties import *
+
+# Helper function to test the wrong types on a property, given its type
+def try_wrong_values(prop):
+ if prop.type != "int":
+ try:
+ prop.set(3)
+ assert False, "Able to insert int value in property of type %s"%prop.type
+ except:
+ pass
+
+ if prop.type != "float":
+ try:
+ prop.set(1.1)
+ assert False, "Able to insert float value in property of type %s"%prop.type
+ except:
+ pass
+
+ if prop.type != "string":
+ try:
+ prop.set("Fake string")
+ assert False, "Able to insert string value in property of type %s"%prop.type
+ except:
+ pass
+
+ if prop.type != "array":
+ try:
+ prop.set([1, 2000, 3, 400])
+ assert False, "Able to insert array value in property of type %s"%prop.type
+ except:
+ pass
+
+ if prop.type != "color":
+ try:
+ prop.set([1,2,3])
+ if prop.type != "array":
+ assert False, "Able to insert color value in property of type %s"%prop.type
+ except:
+ pass
+
+ if prop.type != "boolean":
+ try:
+ prop.set(True)
+ if prop.type != "boolean":
+ assert False, "Able to set boolean value in property of type %s"%prop.type
+ except:
+ pass
+
+class BasePropertyTest(unittest.TestCase):
+ def test_base_class(self):
+ prop = TutoriusProperty()
+
+ assert prop.value == None, "There should not be an initial value in the base property"
+
+ assert prop.type == None, "There should be no type associated with the base property"
+
+ assert prop.get_constraints() == [], "There should be no constraints on the base property"
+
+ prop.set(2)
+
+ assert prop.value == 2, "Unable to set a value on base class"
+
+class TIntPropertyTest(unittest.TestCase):
+ def test_int_property(self):
+ prop = TIntProperty(22)
+
+ assert prop.value == 22, "Could not set value on property via constructor"
+
+ assert prop.type == "int", "Wrong type on int property : %s" % prop.type
+ cons = prop.get_constraints()
+ assert len(cons) == 2, "Not enough constraints on the int property"
+
+ prop.set(12)
+
+ assert prop.value == 12, "Could not set value"
+
+ def test_wrong_values(self):
+ prop = TIntProperty(33)
+
+ # Try setting values of other types
+ try_wrong_values(prop)
+
+ def test_limit_constructor(self):
+ prop = TIntProperty(22, 0, 30)
+
+ try:
+ prop.set(-22)
+ assert False, "Assigning an out-of-range value should trigger LowerLimitConstraint"
+ except LowerLimitConstraintError:
+ pass
+ except Exception:
+ assert False, "Wrong exception triggered by assignation"
+
+ try:
+ prop.set(222)
+ assert False, "Assigning an out-of-range value should trigger UpperLimitConstraint"
+ except UpperLimitConstraintError:
+ pass
+ except Exception:
+ assert False, "Wrong exception triggered by assignation"
+
+ def test_failing_constructor(self):
+ try:
+ prop = TIntProperty(100, 0, 20)
+ assert False, "Creation of the property should fail."
+ except UpperLimitConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+ try:
+ prop = TIntProperty(-100, 0, 20)
+ assert False, "Creation of the property should fail."
+ except LowerLimitConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+class TFloatPropertyTest(unittest.TestCase):
+ def test_float_property(self):
+ prop = TFloatProperty(22)
+
+ assert prop.value == 22, "Could not set value on property via constructor"
+
+ assert prop.type == "float", "Wrong type on float property : %s" % prop.type
+ cons = prop.get_constraints()
+ assert len(cons) == 2, "Not enough constraints on the float property"
+
+ prop.set(12)
+
+ assert prop.value == 12, "Could not set value"
+
+ def test_wrong_values(self):
+ prop = TFloatProperty(33)
+ # Try setting values of other types
+ try_wrong_values(prop)
+
+ def test_limit_constructor(self):
+ prop = TFloatProperty(22.4, 0.1, 30.5223)
+
+ try:
+ prop.set(-22.8)
+ assert False, "Assigning an out-of-range value should trigger LowerLimitConstraint"
+ except LowerLimitConstraintError:
+ pass
+ except Exception:
+ assert False, "Wrong exception triggered by assignation"
+
+ try:
+ prop.set(222.2)
+ assert False, "Assigning an out-of-range value should trigger UpperLimitConstraint"
+ except UpperLimitConstraintError:
+ pass
+ except Exception:
+ assert False, "Wrong exception triggered by assignation"
+
+ def test_failing_constructor(self):
+ try:
+ prop = TFloatProperty(100, 0, 20)
+ assert False, "Creation of the property should fail."
+ except UpperLimitConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+ try:
+ prop = TFloatProperty(-100, 0, 20)
+ assert False, "Creation of the property should fail."
+ except LowerLimitConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+class TStringPropertyTest(unittest.TestCase):
+ def test_basic_string(self):
+ prop = TStringProperty("Starter string")
+
+ assert prop.value == "Starter string", "Could not set string value via constructor"
+
+ assert prop.type == "string", "Wrong type for string property : %s" % prop.type
+
+ def test_size_limit(self):
+ prop = TStringProperty("Small", 10)
+
+ try:
+ prop.set("My string is too big!")
+ assert False, "String should not set to longer than max size"
+ except SizeConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type thrown"
+
+ def test_wrong_values(self):
+ prop = TStringProperty("Beginning")
+
+ try_wrong_values(prop)
+
+ def test_failing_constructor(self):
+ try:
+ prop = TStringProperty("This is normal", 5)
+ assert False, "Creation of the property should fail."
+ except SizeConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+class TArrayPropertyTest(unittest.TestCase):
+ def test_basic_array(self):
+ prop = TArrayProperty([1, 2, 3, 4])
+
+ assert prop.value == [1,2,3,4], "Unable to set initial value via constructor"
+
+ assert prop.type == "array", "Wrong type for array : %s"%prop.type
+
+ def test_wrong_values(self):
+ prop = TArrayProperty([1,2,3,4,5])
+
+ try_wrong_values(prop)
+
+ def test_size_limit(self):
+ prop = TArrayProperty([1,2], 4)
+
+ try:
+ prop.set([1,2,4,5,6,7])
+ assert False, "Size limit constraint was not properly applied"
+ except SizeConstraintError:
+ pass
+ except:
+ assert False, "Wrong type of exception thrown"
+
+
+ def test_failing_constructor(self):
+ try:
+ prop = TArrayProperty([100, 0, 20], 2)
+ assert False, "Creation of the property should fail."
+ except SizeConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+class TColorPropertyTest(unittest.TestCase):
+ def test_basic_color(self):
+ prop = TColorProperty(20, 40, 60)
+
+ assert prop.value == [20, 40, 60], "Could not set initial value with constructor"
+
+ assert prop.type == "color", "Wrong type on color : %s"%prop.type
+
+ def test_wrong_values(self):
+ prop = TColorProperty(250, 250, 250)
+
+ try_wrong_values(prop)
+
+ def test_failing_constructor(self):
+ try:
+ prop = TColorProperty(0, "str", 0)
+ assert False, "Creation of the property should fail."
+ except ColorTypeError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+class TBooleanPropertyTest(unittest.TestCase):
+ def setUp(self):
+ self.prop = TBooleanProperty(False)
+
+ def test_basic_boolean(self):
+ assert self.prop.value == False, "Could not set initial value via constructor"
+
+ self.prop.set(True)
+
+ assert self.prop.value == True, "Could not change the value via set"
+
+ self.prop.set(False)
+
+ assert self.prop.value == False, "Could not change the value via set"
+
+ def test_wrong_types(self):
+ try_wrong_values(self.prop)
+
+ def test_failing_constructor(self):
+ try:
+ prop = TBooleanProperty(64)
+ assert False, "Creation of the property should fail with non-boolean value"
+ except BooleanConstraintError:
+ pass
+ except:
+ assert False, "Wrong exception type on failed constructor"
+
+class TEnumPropertyTest(unittest.TestCase):
+ def setUp(self):
+ self.prop = TEnumProperty("hello", [1, 2, "hello", "world", True, None])
+
+ def test_basic_enum(self):
+ assert self.prop.value == "hello", "Could not set initial value on property"
+
+ self.prop.set(True)
+
+ assert self.prop.value, "Could not change the value via set"
+
+ try:
+ self.prop.set(4)
+ assert False, "Switched to a value outside the enum"
+ except EnumConstraintError:
+ pass
+
+ def test_wrong_type(self):
+ try_wrong_values(self.prop)
+
+class TFilePropertyTest(unittest.TestCase):
+ def setUp(self):
+ self.prop = TFileProperty("propertiestests.py")
+
+ def test_basic_file(self):
+ assert self.prop.value == "propertiestests.py", "Could not set initial value"
+
+ self.prop.set("run-tests.py")
+
+ assert self.prop.value == "run-tests.py", "Could not change value"
+
+ try:
+ self.prop.set("unknown/file/on/disk.gif")
+ assert False, "An exception should be thrown on unknown file"
+ except FileConstraintError:
+ pass
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/src/sugar/tutorius/tests/uamtests.py b/src/sugar/tutorius/tests/uamtests.py
new file mode 100644
index 0000000..b2a5901
--- /dev/null
+++ b/src/sugar/tutorius/tests/uamtests.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@gmail.com>
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@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
+
+import unittest
+
+from sugar.tutorius.uam import parse_uri, SchemeError
+
+PARSE_SUITE={
+#URI SCHEME HOST PARAMS PATH QUERY FRAGMENT
+"tap://act.tut.org/": ["tap", "act.tut.org","", "/", "", ""],
+"tap.gtk://a.t.o/0/1": ["tap.gtk","a.t.o","","/0/1","","",""],
+"tap.gobject://a.t.o/Timer?timeout=5":["tap.gobject","a.t.o","","/Timer","timeout=5",""],
+}
+
+class ParseUriTests(unittest.TestCase):
+ """Tests the UAM parsers"""
+ def test_parse_uri(self):
+ """Test parsing results"""
+ for uri, test in PARSE_SUITE.items():
+ res = parse_uri(uri)
+
+ assert res.scheme == test[0], "%s : Expected scheme %s, got %s" % (uri, test[0], res.scheme)
+ assert res.netloc == test[1], "%s : Expected netloc %s, got %s" % (uri, test[1], res.netloc)
+ assert res.params == test[2], "%s : Expected params %s, got %s" % (uri, test[2], res.params)
+ assert res.path == test[3], "%s : Expected path %s, got %s" % (uri, test[3], res.path)
+ assert res.query == test[4], "%s : Expected query %s, got %s" % (uri, test[4], res.query)
+ assert res.fragment == test[5], "%s : Expected fragment %s, got %s" % (uri, test[5], res.fragment)
+
+ def test_errors(self):
+ """Test exceptions"""
+ try:
+ parse_uri("http://something.org/path")
+ assert False, "Parsing http should fail"
+ except SchemeError:
+ pass
+
+ try:
+ parse_uri("tap.notarealsubscheme://something.org/path")
+ assert False, "Invalid Subscheme should fail"
+ except SchemeError:
+ pass
+
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/src/sugar/tutorius/uam/Makefile.am b/src/sugar/tutorius/uam/Makefile.am
new file mode 100644
index 0000000..219291e
--- /dev/null
+++ b/src/sugar/tutorius/uam/Makefile.am
@@ -0,0 +1,5 @@
+sugardir = $(pythondir)/sugar/tutorius/uam
+sugar_PYTHON = \
+ gobjectparser.py \
+ gtkparser.py \
+ __init__.py
diff --git a/src/sugar/tutorius/uam/__init__.py b/src/sugar/tutorius/uam/__init__.py
new file mode 100644
index 0000000..7cf5671
--- /dev/null
+++ b/src/sugar/tutorius/uam/__init__.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@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
+"""
+Universal Addressing Mechanism module
+
+Allows addressing Events, signals, widgets, etc for supported platforms
+"""
+
+from urllib2 import urlparse
+
+import gtkparser
+import gobjectparser
+
+
+SCHEME="tap" #Tutorius Adressing Protocol
+
+__parsers = {
+ gtkparser.SCHEME:gtkparser.parse_gtk,
+ gobjectparser.SCHEME:gobjectparser.parse_gobject,
+}
+
+def __add_to_urlparse(name):
+ #Add to uses_netloc
+ if not name in urlparse.uses_netloc:
+ urlparse.uses_netloc.append(name)
+
+ #Add to uses_relative
+ if not name in urlparse.uses_relative:
+ urlparse.uses_relative.append(name)
+
+# #Add to uses_params
+# if not name in urlparse.uses_params:
+# urlparse.uses_params.append(name)
+
+ #Add to uses_query
+ if not name in urlparse.uses_query:
+ urlparse.uses_query.append(name)
+
+ #Add to uses_frament
+ if not name in urlparse.uses_fragment:
+ urlparse.uses_fragment.append(name)
+
+
+#Add schemes to urlparse
+__add_to_urlparse(SCHEME)
+
+for subscheme in [".".join([SCHEME,s]) for s in __parsers]:
+ __add_to_urlparse(subscheme)
+
+
+class SchemeError(Exception):
+ def __init__(self, message):
+ Exception.__init__(self, message)
+ self.message = message
+
+
+def parse_uri(uri):
+ res = urlparse.urlparse(uri)
+
+ scheme = res.scheme.split(".")[0]
+ subscheme = ".".join(res.scheme.split(".")[1:])
+ if not scheme == SCHEME:
+ raise SchemeError("Scheme %s not supported" % scheme)
+
+ if subscheme != "" and not subscheme in __parsers:
+ raise SchemeError("SubScheme %s not supported" % subscheme)
+
+ if subscheme:
+ return __parsers[subscheme](res)
+
+ return res
+
+
+
diff --git a/src/sugar/tutorius/uam/gobjectparser.py b/src/sugar/tutorius/uam/gobjectparser.py
new file mode 100644
index 0000000..c1fba3d
--- /dev/null
+++ b/src/sugar/tutorius/uam/gobjectparser.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@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
+"""
+UAM Parser for gobject subscheme
+
+To be completed
+"""
+
+SCHEME="gobject"
+
+def parse_gobject(parsed_uri):
+ """Do nothing for now"""
+ return parsed_uri
diff --git a/src/sugar/tutorius/uam/gtkparser.py b/src/sugar/tutorius/uam/gtkparser.py
new file mode 100644
index 0000000..ede2f03
--- /dev/null
+++ b/src/sugar/tutorius/uam/gtkparser.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@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
+"""
+UAM Parser for gtk subscheme
+
+Allows addressing Gtk Events, signals, widgets
+
+The gtk subscheme for tutorius is
+
+<scheme>://<activity>/<path>[?<params>#<ptype>]
+
+where:
+
+<scheme> is the uam.SCHEME + "." + SCHEME
+
+<activity> is the activity's dns identifier, such as battleship.tutorius.org
+
+<path> is the Hierarchical path to the widget, where 0 is the activity, such as /0/0/1/0/1/0
+
+<params> can be used to specify additionnal parameters required for an event handler or action, such as event=clicked
+
+<ptype> must be used with params to specify which action or eventfilter to use, such as "DialogMessage"
+
+"""
+
+SCHEME="gtk"
+
+def parse_gtk(parsed_uri):
+ """Do nothing for now"""
+ return parsed_uri