Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormike <michael.jmontcalm@gmail.com>2009-02-26 15:46:19 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-02-26 15:46:19 (GMT)
commit3eadc8cee3a83ac98ad526eb11af50d708552e33 (patch)
treed54cc09643fd6e126cd75bd7a78cecc35e18eda2
parent8d2fb3bad1ccf6866d69ecb251d7df6773930c7b (diff)
parent5da17d78095187c656002210d937b04cd41f159c (diff)
Merge branch 'tutorial_toolkit' of ssh://mike@bobthebuilder.mine.nu:8080/home/git into tutorial_toolkit
Conflicts: source/external/source/sugar-toolkit/src/sugar/tutorius/tutorial.py
-rw-r--r--src/sugar/tutorius/core.py343
-rw-r--r--src/sugar/tutorius/dialog.py24
-rw-r--r--src/sugar/tutorius/tutorial.py279
3 files changed, 364 insertions, 282 deletions
diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py
new file mode 100644
index 0000000..8919057
--- /dev/null
+++ b/src/sugar/tutorius/core.py
@@ -0,0 +1,343 @@
+# 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
+"""
+Module Tutorial
+
+This is the main module for Tutorius.
+
+"""
+
+import gtk
+import logging
+
+from sugar.tutorius.dialog import TutoriusDialog
+
+
+logger = logging.getLogger("tutorius")
+
+class Event:
+ """Event descriptor class.
+ This class is used to describe events that are expected to happen.
+ """
+
+ def __init__(self, object_name, event_name ):
+ """Constructor for Event
+ @param object_name str name of the object that will send the event
+ @param event_name str name of the event
+
+ Example:
+ evt=Event("0.0.1.1.2", "clicked")
+ """
+ self.object_name = object_name
+ self.event_name = event_name
+
+ def test(self, sig, name):
+ """Utility method for testing the equality between a signal and object
+ names and their expected values.
+ @param sig str signal name
+ @param name str object name
+ @return True if both match the expected values, False otherwise
+ """
+ if self.object_name == name and self.event_name == sig:
+ return True
+ return False
+
+
+class Tutorial (object):
+ """
+ Tutorial Class, used to run through the FSM.
+ """
+ EVENTS = [
+ "focus",
+ "button-press-event",
+ "enter-notify-event",
+ "leave-notify-event",
+ "key-press-event",
+ "text-selected",
+ "clicked",
+ ]
+
+ IGNORED_WIDGETS = [
+ "GtkVBox",
+ "GtkHBox",
+ "GtkAlignment",
+ "GtkNotebook",
+ "GtkButton",
+ "GtkToolItem",
+ "GtkToolbar",
+ ]
+
+ def __init__(self, name, fsm):
+ """Create an unattached tutorial
+ """
+ object.__init__(self)
+ self.name = name
+ self.state_machine = fsm
+ self.state = None
+
+ self.handlers = []
+ self.activity = None
+ #Rest of initialisation happens when attached
+
+ def attach(self, activity):
+ """Attach to a running activity
+ @param activity the activity to attach to
+ """
+ #For now, absolutely detach if a previous one!
+ if self.activity:
+ self.detach()
+ self.activity = activity
+ self.set_state("INIT")
+
+ def detach(self):
+ """Detach from the current activity"""
+ self.disconnect_handlers()
+ self.activity = None
+
+ def handle_event(self, *args):
+ """Default event handler for the Tutorial.
+ Tests the received object and signal names onto each defined
+ transition and changes to the next state if successful.
+
+ The last parameter should be a two-tuple containing the
+ (signal_name, object_name)
+ """
+ sig, objname = args[-1]
+ logger.debug("EVENT %s ON %s" % (sig, objname) )
+ for transition, next in self.state_machine[self.state]["Events"]:
+ if transition.test(sig, objname):
+ self.set_state(next)
+
+# @staticmethod
+# def logEvent(obj, *args):
+# logger.debug("%s" % str(args[-1]))
+
+ def disconnect_handlers(self):
+ """Disconnect all event handlers attached by self"""
+ #Loop through handlers
+ for obj, hid in self.handlers:
+ obj.handler_disconnect(hid)
+ self.handlers = []
+
+
+ def set_state(self, name):
+ """Switch to a new state"""
+ if not self.state_machine.has_key(name):
+ return
+ logger.debug("====NEW STATE: %s====" % name)
+ self.disconnect_handlers()
+ self.state = name
+ newstate = self.state_machine.get(name)
+ for event, unused in newstate["Events"]:
+ self.register_signal(self.handle_event, \
+ event.object_name, event.event_name)
+
+ if newstate.has_key("Message"):
+ dlg = TutoriusDialog(newstate["Message"])
+ dlg.set_button_clicked_cb(dlg.close_self)
+ dlg.run()
+
+
+ def register_signals(self, 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
+ argument which is a two-tuple containing the signal name and
+ 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.
+
+ 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
+ """
+ #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 Tutorial.IGNORED_WIDGETS)] \
+ )
+ self.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)] \
+ )
+ #register events on the target if a widget XXX necessary to check this?
+ if isinstance(target, gtk.Widget):
+ for sig in Tutorial.EVENTS:
+ try:
+ self.handlers.append( \
+ (target, target.connect(sig, handler, (sig, name) )) \
+ )
+ except TypeError:
+ continue
+
+ def register_signals_numbered(self, \
+ target, handler, prefix="0", max_depth=None):
+ """
+ Recursive function to register event handlers on an target
+ and it's children. The event handler is called with an extra
+ argument which is a two-tuple containing the signal name and
+ the FQDN-style name of the target that triggered the event.
+
+ This function registers all of the events listed in
+ Tutorial.EVENTS
+
+ Example arg tuple added:
+ ("focus", "1.1.2")
+ 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
+ """
+ #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
+ self.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:
+ try:
+ self.handlers.append( \
+ (target, target.connect(sig, handler, (sig, prefix) ))\
+ )
+ except TypeError:
+ continue
+
+ def register_signal(self, handler, obj_fqdn, signal_name):
+ """Register a signal handler onto a specific widget
+ @param handler function to attach as a handler
+ @param obj_fqdn fqdn-style object name
+ @param signal_name signal name to connect to
+
+ Side effects:
+ the object found and the handler id obtained by connect() are
+ appended in self.handlers
+ """
+ path = obj_fqdn.split(".")
+ #We select the first object and pop the first zero
+ obj = self.activity
+ path.pop(0)
+
+ while len(path) > 0:
+ obj = obj.get_children()[int(path.pop(0))]
+
+ self.handlers.append( \
+ (obj, obj.connect(signal_name, handler, (signal_name, obj_fqdn) ))\
+ )
+
+class State:
+ """This is a step in a tutorial. The state represents a collection of
+ actions to undertake when entering the state, and a description of an
+ event filter with associated actions to go to the next state."""
+
+ def __init__(self):
+ """Initializes the content of the state, as in loading the actions
+ that are required and building the correct tests."""
+ self.actions = []
+ self.tests = []
+
+
+ def setup(self):
+ """Install the state itself. This is the best time to pop-up a dialog
+ that has to remain for the duration of the state."""
+ for act in self.actions:
+ act.do()
+
+
+ def teardown(self):
+ """Undo every action that was installed for this state. This means
+ removing dialogs that were displayed, removing highlights, etc..."""
+ for act in self.actions:
+ act.undo()
+
+
+ def verify(self):
+ """Run the internal tests to see if one of them passes. If it does,
+ then do the associated processing to go in the next state."""
+ for test in self.tests:
+ if test.verify() == True:
+ actions = test.get_actions()
+ for act in actions:
+ act.do()
+ # Now that we execute the actions related to a test, we might
+ # want to undo them right after --- should we use a callback or
+ # a timer?
+
+class FiniteStateMachine(State):
+ """This is a collection of states, with a start state and an end callback.
+ It is used to simplify the development of the various tutorials by
+ encapsulating a collection of states that represent a given learning
+ process."""
+ def __init__(self, start_state, setup_actions):
+ """The constructor for a FSM. Pass in the start state and the setup
+ actions that need to be taken when the FSM itself start (which may be
+ different from what is done in the first state of the machine)."""
+ State.__init__(self)
+
+ self.start_state = start_state
+ self.actions = setup_actions
+
+ self.current_state = self.start_state
+ #TODO Setup current state now?
+
+ def setup(self):
+ """
+ Set up the FSM
+ """
+ for act in self.actions:
+ act.do()
+
+ def teardown(self):
+ """
+ Revert any changes done by setup()
+ """
+ for act in self.actions:
+ act.undo()
+
+ def verify(self):
+ "Verify if the current state passes it's tests"""
+ return self.current_state.verify()
diff --git a/src/sugar/tutorius/dialog.py b/src/sugar/tutorius/dialog.py
index 298800a..be51a0e 100644
--- a/src/sugar/tutorius/dialog.py
+++ b/src/sugar/tutorius/dialog.py
@@ -13,11 +13,27 @@
# 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
-
+"""
+The Dialog module provides means of interacting with the user
+through the use of Dialogs.
+"""
import gtk
class TutoriusDialog(gtk.Dialog):
+ """
+ TutoriusDialog is a simple wrapper around gtk.Dialog.
+
+ It allows creating and showing a dialog and connecting the response and
+ button click events to callbacks.
+ """
def __init__(self, label="Hint", button_clicked_cb=None, response_cb=None):
+ """
+ Constructor.
+
+ @param label text to be shown on the dialog
+ @param button_clicked_cb callback for the button click
+ @param response_cb callback for the dialog response
+ """
gtk.Dialog.__init__(self)
self._button = gtk.Button(label)
@@ -34,8 +50,10 @@ class TutoriusDialog(gtk.Dialog):
self.set_decorated(False)
- def setButtonClickedCallback(self, funct):
+ def set_button_clicked_cb(self, funct):
+ """Setter for the button_clicked callback"""
self._button.connect("clicked", funct)
- def closeSelf(self, Arg=None):
+ def close_self(self, arg=None):
+ """Close the dialog"""
self.destroy()
diff --git a/src/sugar/tutorius/tutorial.py b/src/sugar/tutorius/tutorial.py
deleted file mode 100644
index 8c457ae..0000000
--- a/src/sugar/tutorius/tutorial.py
+++ /dev/null
@@ -1,279 +0,0 @@
-# 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
-
-
-import gtk
-import logging
-
-from sugar.tutorius.dialog import TutoriusDialog
-
-
-logger = logging.getLogger("tutorius")
-
-class Event:
- def __init__(self, object_name, event_name ):
- self.object_name = object_name
- self.event_name = event_name
-
- def test(self, sig, name):
- if self.object_name == name and self.event_name == sig:
- return True
- return False
-
-
-class Tutorial (object):
- EVENTS = [
- "focus",
- "button-press-event",
- "enter-notify-event",
- "leave-notify-event",
- "key-press-event",
- "text-selected",
- "clicked",
- ]
-
- IGNORED_WIDGETS = [
- "GtkVBox",
- "GtkHBox",
- "GtkAlignment",
- "GtkNotebook",
- "GtkButton",
- "GtkToolItem",
- "GtkToolbar",
- ]
-
- def __init__(self, name, fsm):
- object.__init__(self)
- self.name = name
- self.state_machine = fsm
-
- self.handlers = []
- self.activity = None
- #self.setState("INIT")
- #self.state="INIT"
- #self.register_signals(self.activity, self.handleEvent, max_depth=10)
-
- def attach(self, activity):
- #For now, absolutely detach if a previous one!
- if self.activity:
- self.detach()
- self.activity = activity
- self.state="INIT"
- self.register_signals(self.activity,self.handleEvent, max_depth=10)
-
- def detach(self):
- self.disconnectHandlers()
- self.activity = None
-
- def handleEvent(self, *args):
- sig, objname = args[-1]
- logger.debug("EVENT %s ON %s" % (sig, objname) )
- for transition, next in self.state_machine[self.state]["Events"]:
- if transition.test(sig,objname):
- logger.debug("====NEW STATE: %s====" % next)
- self.state = next
- dlg = TutoriusDialog(self.state_machine[self.state]["Message"])
- dlg.setButtonClickedCallback(dlg.closeSelf)
- dlg.run()
-
-# @staticmethod
-# def logEvent(obj, *args):
-# logger.debug("%s" % str(args[-1]))
-
- def disconnectHandlers(self):
- for t, id in self.handlers:
- t.disconnect_handler(id)
-
-# def setState(self,name):
-# self.disconnectHandlers()
-# self.state = name
-# newstate = ABIWORD_MEF.get(name,())
-# for event, n in newstate:
-# target = self.activity
-# try:
-# for obj in event.object_name.split("."):
-# target = getattr(target,obj)
-# id = target.connect(self.handler,(event.object_name, event.event_name))
-# self.handlers.append(target, id)
-# id = target.connect(Tutorial.logEvent,"EVENT %s ON %s" % (event.object_name, event.event_name))
-# self.handlers.append(target, id)
-# except Exception, e:
-# logger.debug(str(e))
-
- def register_signals(self,object,handler,prefix=None,max_depth=None):
- """
- Recursive function to register event handlers on an object
- and it's children. The event handler is called with an extra
- argument which is a two-tuple containing the signal name and
- the FQDN-style name of the object 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.
-
- Example arg tuple added:
- ("focus", "Activity.Toolbox.Bold")
- Side effects:
- -Handlers connected on the various objects
- -Handler ID's stored in self.handlers
-
- @param object the object to recurse on
- @param handler the handler function to connect
- @param prefix name prepended to the object name to form a chain
- @param max_depth maximum recursion depth, None for infinity
- """
- #Gtk Containers have a get_children() function
- if hasattr(object,"get_children") and \
- hasattr(object.get_children,"__call__"):
- for child in object.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, object.get_name()) \
- if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
- )
- self.register_signals(child,handler,pre,max_depth-1)
- name = ".".join( \
- [p for p in (prefix, object.get_name()) \
- if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
- )
- #register events on the object if a widget XXX necessary to check this?
- if isinstance(object,gtk.Widget):
- for sig in Tutorial.EVENTS:
- try:
- self.handlers.append( (object,object.connect(sig,handler,(sig, name) )) )
- except TypeError:
- continue
-
-
-###############################################################################
-#
-# Object oriented model for the FSM
-#
-
-class Action:
- """Represents an action to take when entering a state. An action might be
- show a dialog to the user, or to play a sound.
-
- The do() executes the interaction, while the undo() must clean up
- everything. """
- def __init__(self):
- self.name = "Default Action"
-
- def do(self):
- logging.debug("Doing default action")
-
- def undo(self):
- logging.debug("Undoing default action")
-
-class DialogAction(Action):
- """This is a pop-up dialog that displays a short text to the user."""
- def __init__(self, label, posX, posY):
- self.name = "Dialog Action"
- self.label = label
- self.pos = [posX, posY]
-
- def do(self):
- self.dialog = TutoriusDialog(label)
- self.dialog.move(self.pos[0], self,pos[1])
-
- def undo(self):
- self.dialog.destroy()
-
-class State:
- """This is a step in a tutorial. The state represents a collection of
- actions to undertake when entering the state, and a description of an
- event filter with associated actions to go to the next state."""
-
- def __init__(self):
- """Initializes the content of the state, as in loading the actions
- that are required and building the correct tests."""
- self.actions = []
- self.tests = []
-
-
- def setup(self):
- """Install the state itself. This is the best time to pop-up a dialog
- that has to remain for the duration of the state."""
- for act in self.actions:
- act.do()
-
-
- def teardown(self):
- """Undo every action that was installed for this state. This means
- removing dialogs that were displayed, removing highlights, etc..."""
- for act in self.actions:
- act.undo()
-
-
- def verify(self):
- """Run the internal tests to see if one of them passes. If it does,
- then do the associated processing to go in the next state."""
- for test in self.tests:
- if test.verify() == True:
- actions = test.get_actions()
- for act in actions:
- act.do()
- # Now that we execute the actions related to a test, we might
- # want to undo them right after --- should we use a callback or
- # a timer?
-
-class FiniteStateMachine(State):
- """This is a collection of states, with a start state and an end callback.
- It is used to simplify the development of the various tutorials by
- encapsulating a collection of states that represent a given learning
- process."""
- def __init__(self, start_state, setup_actions):
- """The constructor for a FSM. Pass in the start state and the setup
- actions that need to be taken when the FSM itself start (which may be
- different from what is done in the first state of the machine)."""
- self.start_state = start_state
- self.actions = setup_actions
-
- self.tests = []
-
- self.current_state = self.start_state
-
- def setup(self):
- for act in self.actions:
- act.do()
-
- def teardown(self):
- for act in self.actions:
- act.undo
-
- def verify(self):
- return self.current_state.verify()
-
-
-class Executor:
- """This is a class that executes a tutorial graph, meaning that it handles
- the creation and deletion of states, as well as handling the transitions
- between the various states."""
-
- def __init__(self):
- self.current_state = None
-
-
- def start(self, fsm):
- if self.current_state == None:
- self.current_state = fsm
-
- self.current_state.install_handlers()
- self.current_state.setup()
-
- \ No newline at end of file