Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tutorius/core.py
diff options
context:
space:
mode:
authorSimon Poirier <simpoir@gmail.com>2009-07-11 21:39:46 (GMT)
committer Simon Poirier <simpoir@gmail.com>2009-07-11 22:00:30 (GMT)
commit0c3f127c86af818d260966d2292b199757087157 (patch)
tree62cf941aef5bde83641a17ec492e03d0ecb17386 /tutorius/core.py
parent9fafb49af210e956d43d6a00106558d1a00d13df (diff)
repackage
Diffstat (limited to 'tutorius/core.py')
-rw-r--r--tutorius/core.py528
1 files changed, 528 insertions, 0 deletions
diff --git a/tutorius/core.py b/tutorius/core.py
new file mode 100644
index 0000000..dd2435e
--- /dev/null
+++ b/tutorius/core.py
@@ -0,0 +1,528 @@
+# 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
+import copy
+import os
+
+from sugar.tutorius.dialog import TutoriusDialog
+from sugar.tutorius.gtkutils import find_widget
+from sugar.tutorius.services import ObjectStore
+
+logger = logging.getLogger("tutorius")
+
+class Tutorial (object):
+ """
+ Tutorial Class, used to run through the 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)
+
+ 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
+ ObjectStore().activity = activity
+ ObjectStore().tutorial = self
+ self._prepare_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())
+
+ 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):
+ """
+ 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=None, event_filter_list=None, 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
+ """
+ object.__init__(self)
+
+ self.name = name
+
+ self._actions = action_list or []
+
+ # Unused for now
+ #self.tests = []
+
+ self._event_filters = event_filter_list or []
+
+ 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())
+
+ # Model manipulation
+ # These functions are used to simplify the creation of states
+ def add_action(self, new_action):
+ """
+ Adds an action to the state (only if it wasn't added before)
+
+ @param new_action The new action to execute when in this state
+ @return True if added, False otherwise
+ """
+ if new_action not in self._actions:
+ self._actions.append(new_action)
+ return True
+ return False
+
+ # remove_action - We did not define names for the action, hence they're
+ # pretty hard to remove on a precise basis
+
+ def get_action_list(self):
+ """
+ @return A list of actions that the state will execute
+ """
+ return self._actions
+
+ def clear_actions(self):
+ """
+ Removes all the action associated with this state. A cleared state will
+ not do anything when entered or exited.
+ """
+ self._actions = []
+
+ def add_event_filter(self, event_filter):
+ """
+ Adds an event filter that will cause a transition from this state.
+
+ The same event filter may not be added twice.
+
+ @param event_filter The new event filter that will trigger a transition
+ @return True if added, False otherwise
+ """
+ if event_filter not in self._event_filters:
+ self._event_filters.append(event_filter)
+ return True
+ return False
+
+ def get_event_filter_list(self):
+ """
+ @return The list of event filters associated with this state.
+ """
+ return self._event_filters
+
+ def clear_event_filters(self):
+ """
+ Removes all the event filters associated with this state. A state that
+ was just cleared will become a sink and will be the end of the
+ tutorial.
+ """
+ self._event_filters = []
+
+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=None, start_state_name="INIT", action_list=None):
+ """
+ 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 or {}
+
+ self.start_state_name = start_state_name
+ # Set the current state to None - we are not executing anything yet
+ self.current_state = None
+
+ # 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 or []
+
+ # 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:
+ # Remember the initial state - we might want to reset
+ # or rewind the FSM at a later moment
+ self.start_state = self._states[self.start_state_name]
+ self.current_state = self.start_state
+ # 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
+
+ if self.current_state != None:
+ if new_state_name == self.current_state.name:
+ # If we already are in this state, we do not need to change
+ # anything in the current state - By design, a state may not point
+ # to itself
+ 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 get_current_state_name(self):
+ """
+ Returns the name of the current state.
+
+ @return A string representing the name of the current state
+ """
+ return self.current_state.name
+
+ def teardown(self):
+ """
+ Revert any changes done by setup()
+ """
+ # Teardown the current state
+ if self.current_state is not None:
+ 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()
+
+ # TODO : It might be nice to have a start() and stop() method for the
+ # FSM.
+
+ # Data manipulation section
+ # These functions are dedicated to the building and editing of a graph.
+ def add_state(self, new_state):
+ """
+ Inserts a new state in the FSM.
+
+ @param new_state The State object that will now be part of the FSM
+ @raise KeyError In the case where a state with this name already exists
+ """
+ if self._states.has_key(new_state.name):
+ raise KeyError("There is already a state by this name in the FSM")
+
+ self._states[new_state.name] = new_state
+
+ # Not such a great name for the state accessor... We already have a
+ # set_state name, so get_state would conflict with the notion of current
+ # state - I would recommend having a set_current_state instead.
+ def get_state_by_name(self, state_name):
+ """
+ Fetches a state from the FSM, based on its name. If there is no
+ such state, the method will throw a KeyError.
+
+ @param state_name The name of the desired state
+ @return The State object having the given name
+ """
+ return self._states[state_name]
+
+ def remove_state(self, state_name):
+ """
+ Removes a state from the FSM. Raises a KeyError when the state is
+ not existent.
+
+ Warning : removing a state will also remove all the event filters that
+ point to this given name, to preserve the FSM's integrity. If you only
+ want to edit a state, you would be better off fetching this state with
+ get_state_by_name().
+
+ @param state_name A string being the name of the state to remove
+ @raise KeyError When the state_name does not a represent a real state
+ stored in the dictionary
+ """
+
+ state_to_remove = self._states[state_name]
+
+ # Remove the state from the states' dictionnary
+ for st in self._states.itervalues():
+ # Iterate through the list of event filters and remove those
+ # that point to the state that will be removed
+
+ #TODO : Move this code inside the State itself - we're breaking
+ # encap :P
+ for event_filter in st._event_filters:
+ if event_filter.get_next_state() == state_name:
+ st._event_filters.remove(event_filter)
+
+ # Remove the state from the dictionary
+ del self._states[state_name]
+
+ # Exploration methods - used to know more about a given state
+ def get_following_states(self, state_name):
+ """
+ Returns a tuple of the names of the states that point to the given
+ state. If there is no such state, the function raises a KeyError.
+
+ @param state_name The name of the state to analyse
+ @raise KeyError When there is no state by this name in the FSM
+ """
+ state = self._states[state_name]
+
+ next_states = set()
+
+ for event_filter in state._event_filters:
+ next_states.add(event_filter.get_next_state())
+
+ return tuple(next_states)
+
+ def get_previous_states(self, state_name):
+ """
+ Returns a tuple of the names of the state that can transition to
+ the given state. If there is no such state, the function raises a
+ KeyError.
+
+ @param state_name The name of the state that the returned states might
+ transition to.
+ """
+ # This might seem a bit funny, but we don't verify if the given
+ # state is present or not in the dictionary.
+ # This is due to the fact that when building a graph, we might have a
+ # prototypal state that has not been inserted yet. We could not know
+ # which states are pointing to it until we insert it in the graph.
+
+ states = []
+ # Walk through the list of states
+ for st in self._states.itervalues():
+ for event_filter in st._event_filters:
+ if event_filter.get_next_state() == state_name:
+ states.append(event_filter.get_next_state())
+ continue
+
+ return tuple(states)
+
+ # Convenience methods to see the content of a FSM
+ def __str__(self):
+ out_string = ""
+ for st in self._states.itervalues():
+ out_string += st.name + ", "
+ return out_string