Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugar-toolkit/src/sugar/tutorius/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'sugar-toolkit/src/sugar/tutorius/core.py')
-rw-r--r--sugar-toolkit/src/sugar/tutorius/core.py334
1 files changed, 334 insertions, 0 deletions
diff --git a/sugar-toolkit/src/sugar/tutorius/core.py b/sugar-toolkit/src/sugar/tutorius/core.py
new file mode 100644
index 0000000..f817ba9
--- /dev/null
+++ b/sugar-toolkit/src/sugar/tutorius/core.py
@@ -0,0 +1,334 @@
+# 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
+"""
+Core
+
+This module contains the core classes for tutorius
+
+"""
+
+import gtk
+import logging
+
+from sugar.tutorius.dialog import TutoriusDialog
+from sugar.tutorius.gtkutils import find_widget
+
+logger = logging.getLogger("tutorius")
+
+class Tutorial (object):
+ """
+ Tutorial Class, used to run through the FSM.
+ """
+
+ def __init__(self, name, fsm):
+ """
+ Creates an unattached tutorial.
+ """
+ object.__init__(self)
+ self.name = name
+
+ self.state_machine = fsm
+ self.state_machine.set_tutorial(self)
+
+ 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.state_machine.set_state("INIT")
+
+ def detach(self):
+ """
+ Detach from the current activity
+ """
+
+ # Uninstall the whole FSM
+ self.state_machine.teardown()
+
+ #FIXME There should be some amount of resetting done here...
+ self.activity = None
+
+
+ def set_state(self, name):
+ """
+ Switch to a new state
+ """
+ logger.debug("====NEW STATE: %s====" % name)
+
+ self.state_machine.set_state(name)
+
+
+ # Currently unused -- equivalent function is in each state
+ def _eventfilter_state_done(self, eventfilter):
+ """
+ Callback handler for eventfilter to notify
+ when we must go to the next state.
+ """
+ #XXX Tests should be run here normally
+
+ #Swith to the next state pointed by the eventfilter
+ self.set_state(eventfilter.get_next_state())
+
+class State:
+ """
+ This is a step in a tutorial. The state represents a collection of actions
+ to undertake when entering the state, and a series of event filters
+ with associated actions that point to a possible next state.
+ """
+
+ def __init__(self, name, action_list=[], event_filter_list=[], tutorial=None):
+ """
+ Initializes the content of the state, like loading the actions
+ that are required and building the correct tests.
+
+ @param action_list The list of actions to execute when entering this
+ state
+ @param event_filter_list A list of tuples of the form
+ (event_filter, next_state_name), that explains the outgoing links for
+ this state
+ @param tutorial The higher level container of the state
+ """
+ self._actions = action_list
+
+ # Unused for now
+ #self.tests = []
+
+ self._event_filters = event_filter_list
+
+ self.tutorial = tutorial
+
+ def set_tutorial(self, tutorial):
+ """
+ Associates this state with a tutorial. A tutorial must be set prior
+ to executing anything in the state. The reason for this is that the
+ states need to have access to the activity (via the tutorial) in order
+ to properly register their callbacks on the activities' widgets.
+
+ @param tutorial The tutorial that this state runs under.
+ """
+ if self.tutorial == None :
+ self.tutorial = tutorial
+ else:
+ raise RuntimeWarning(\
+ "The state %s was already associated with a tutorial." % self.name)
+
+ def setup(self):
+ """
+ Install the state itself, by first registering the event filters
+ and then triggering the actions.
+ """
+ for eventfilter in self._event_filters:
+ eventfilter.install_handlers(self._event_filter_state_done_cb,
+ activity=self.tutorial.activity)
+
+ for action in self._actions:
+ action.do()
+
+ def teardown(self):
+ """
+ Uninstall all the event filters that were active in this state.
+ Also undo every action that was installed for this state. This means
+ removing dialogs that were displayed, removing highlights, etc...
+ """
+ # Remove the handlers for the all of the state's event filters
+ for event_filter in self._event_filters:
+ event_filter.remove_handlers()
+
+ # Undo all the actions related to this state
+ for action in self._actions:
+ action.undo()
+
+ def _event_filter_state_done_cb(self, event_filter):
+ """
+ Callback for event filters. This function needs to inform the
+ tutorial that the state is over and tell it what is the next state.
+
+ @param event_filter The event filter that was called
+ """
+ # Run the tests here, if need be
+
+ # Warn the higher level that we wish to change state
+ self.tutorial.set_state(event_filter.get_next_state())
+
+ # Unused for now
+## 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.
+
+ For now, we will consider that there can only be states
+ inserted in the FSM, and that there are no nested FSM inside.
+ """
+
+ def __init__(self, name, tutorial=None, state_dict={}, start_state_name="INIT", action_list=[]):
+ """
+ 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).
+
+ @param name A short descriptive name for this FSM
+ @param tutorial The tutorial that will execute this FSM. If None is
+ attached on creation, then one must absolutely be attached before
+ executing the FSM with set_tutorial().
+ @param state_dict A dictionary containing the state names as keys and
+ the state themselves as entries.
+ @param start_state_name The name of the starting state, if different
+ from "INIT"
+ @param action_list The actions to undertake when initializing the FSM
+ """
+ State.__init__(self, name)
+
+ self.name = name
+ self.tutorial = tutorial
+
+ # Dictionnary of states contained in the FSM
+ self._states = state_dict
+
+ # Remember the initial state - we might want to reset
+ # or rewind the FSM at a later moment
+ self.start_state = state_dict[start_state_name]
+ self.current_state = self.start_state
+
+ # Register the actions for the FSM - They will be processed at the
+ # FSM level, meaning that when the FSM will start, it will first
+ # execute those actions. When the FSM closes, it will tear down the
+ # inner actions of the state, then close its own actions
+ self.actions = action_list
+
+ # Flag to mention that the FSM was initialized
+ self._fsm_setup_done = False
+ # Flag that must be raised when the FSM is to be teared down
+ self._fsm_teardown_done = False
+ # Flag used to declare that the FSM has reached an end state
+ self._fsm_has_finished = False
+
+ def set_tutorial(self, tutorial):
+ """
+ This associates the FSM to the given tutorial. It MUST be associated
+ either in the constructor or with this function prior to executing the
+ FSM.
+
+ @param tutorial The tutorial that will execute this FSM.
+ """
+ # If there was no tutorial associated
+ if self.tutorial == None:
+ # Associate it with this FSM and all the underlying states
+ self.tutorial = tutorial
+ for state in self._states.itervalues():
+ state.set_tutorial(tutorial)
+ else:
+ raise RuntimeWarning(\
+ "The FSM %s is already associated with a tutorial."%self.name\
+ )
+
+ def setup(self):
+ """
+ This function initializes the FSM the first time it is called.
+ Then, every time it is called, it initializes the current state.
+ """
+ # Are we associated with a tutorial?
+ if self.tutorial == None:
+ raise UnboundLocalError("No tutorial was associated with FSM %s" % self.name)
+
+ # If we never initialized the FSM itself, then we need to run all the
+ # actions associated with the FSM.
+ if self._fsm_setup_done == False:
+ # Flag the FSM level setup as done
+ self._fsm_setup_done = True
+ # Execute all the FSM level actions
+ for action in self.actions:
+ action.do()
+
+ # Then, we need to run the setup of the current state
+ self.current_state.setup()
+
+ def set_state(self, new_state_name):
+ """
+ This functions changes the current state of the finite state machine.
+
+ @param new_state The identifier of the state we need to go to
+ """
+ # TODO : Since we assume no nested FSMs, we don't set state on the
+ # inner States / FSMs
+## # Pass in the name to the internal state - it might be a FSM and
+## # this name will apply to it
+## self.current_state.set_state(new_state_name)
+
+ # Make sure the given state is owned by the FSM
+ if not self._states.has_key(new_state_name):
+ # If we did not recognize the name, then we do not possess any
+ # state by that name - we must ignore this state change request as
+ # it will be done elsewhere in the hierarchy (or it's just bogus).
+ return
+
+ new_state = self._states[new_state_name]
+
+ # Undo the actions of the old state
+ self.teardown()
+
+ # Insert the new state
+ self.current_state = new_state
+
+ # Call the initial actions in the new state
+ self.setup()
+
+
+ def teardown(self):
+ """
+ Revert any changes done by setup()
+ """
+ # Teardown the current state
+ self.current_state.teardown()
+
+ # If we just finished the whole FSM, we need to also call the teardown
+ # on the FSM level actions
+ if self._fsm_has_finished == True:
+ # Flag the FSM teardown as not needed anymore
+ self._fsm_teardown_done = True
+ # Undo all the FSM level actions here
+ for action in self.actions:
+ action.undo()
+
+ #Unused for now
+## def verify(self):
+## """Verify if the current state passes its tests"""
+## return self.current_state.verify()