diff options
Diffstat (limited to 'sugar-toolkit/src/sugar/tutorius/core.py')
-rw-r--r-- | sugar-toolkit/src/sugar/tutorius/core.py | 334 |
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() |