diff options
Diffstat (limited to 'src/sugar/tutorius/core.py')
-rw-r--r-- | src/sugar/tutorius/core.py | 231 |
1 files changed, 200 insertions, 31 deletions
diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py index 14bb751..901820f 100644 --- a/src/sugar/tutorius/core.py +++ b/src/sugar/tutorius/core.py @@ -23,6 +23,7 @@ This module contains the core classes for tutorius import gtk import logging +import copy from sugar.tutorius.dialog import TutoriusDialog from sugar.tutorius.gtkutils import find_widget @@ -81,7 +82,7 @@ class Tutorial (object): """ Switch to a new state """ - logger.debug("====NEW STATE: %s====" % name) + logger.debug("==== NEW STATE: %s ====" % name) self.state_machine.set_state(name) @@ -97,14 +98,14 @@ class Tutorial (object): #Swith to the next state pointed by the eventfilter self.set_state(eventfilter.get_next_state()) -class State: +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=[], event_filter_list=[], tutorial=None): + 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. @@ -116,12 +117,16 @@ class State: this state @param tutorial The higher level container of the state """ - self._actions = action_list + object.__init__(self) + + self.name = name + + self._actions = action_list or [] # Unused for now #self.tests = [] - self._event_filters = event_filter_list + self._event_filters = event_filter_list or [] self.tutorial = tutorial @@ -177,19 +182,64 @@ class State: # 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 - # 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? + 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): """ @@ -202,7 +252,7 @@ class FiniteStateMachine(State): 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=[]): + 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 @@ -224,18 +274,20 @@ class FiniteStateMachine(State): self.tutorial = tutorial # Dictionnary of states contained in the FSM - self._states = state_dict + self._states = state_dict or {} - # 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 + self.start_state_name = start_state_name + # If we have a filled input dictionary + if len(self._states) > 0: + self.current_state = self._states[self.start_state_name] + else: + 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 + self.actions = action_list or [] # Flag to mention that the FSM was initialized self._fsm_setup_done = False @@ -260,8 +312,7 @@ class FiniteStateMachine(State): state.set_tutorial(tutorial) else: raise RuntimeWarning(\ - "The FSM %s is already associated with a tutorial."%self.name\ - ) + "The FSM %s is already associated with a tutorial."%self.name) def setup(self): """ @@ -275,6 +326,10 @@ class FiniteStateMachine(State): # 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 @@ -314,13 +369,21 @@ class FiniteStateMachine(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 - self.current_state.teardown() + 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 @@ -331,7 +394,113 @@ class FiniteStateMachine(State): 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() + # 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
\ No newline at end of file |