diff options
Diffstat (limited to 'tutorius/core.py')
-rw-r--r-- | tutorius/core.py | 190 |
1 files changed, 107 insertions, 83 deletions
diff --git a/tutorius/core.py b/tutorius/core.py index 41089f1..6030457 100644 --- a/tutorius/core.py +++ b/tutorius/core.py @@ -21,14 +21,12 @@ 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 +from sugar.tutorius.TProbe import ProbeManager +from sugar.tutorius.dbustools import save_args +from sugar.tutorius import addon logger = logging.getLogger("tutorius") @@ -36,8 +34,11 @@ class Tutorial (object): """ Tutorial Class, used to run through the FSM. """ + #Properties + probeManager = property(lambda self: self._probeMgr) + activityId = property(lambda self: self._activity_id) - def __init__(self, name, fsm,filename= None): + def __init__(self, name, fsm, filename=None): """ Creates an unattached tutorial. """ @@ -51,21 +52,22 @@ class Tutorial (object): self.state = None self.handlers = [] - self.activity = None + self._probeMgr = ProbeManager() + self._activity_id = None #Rest of initialisation happens when attached - def attach(self, activity): + def attach(self, activity_id): """ Attach to a running activity - @param activity the activity to attach to + @param activity_id the id of the activity to attach to """ #For now, absolutely detach if a previous one! - if self.activity: + if self._activity_id: self.detach() - self.activity = activity - ObjectStore().activity = activity - ObjectStore().tutorial = self + self._activity_id = activity_id + self._probeMgr.attach(activity_id) + self._probeMgr.currentActivity = activity_id self._prepare_activity() self.state_machine.set_state("INIT") @@ -77,9 +79,9 @@ class Tutorial (object): # Uninstall the whole FSM self.state_machine.teardown() - #FIXME There should be some amount of resetting done here... - self.activity = None - + if not self._activity_id is None: + self._probeMgr.detach(self._activity_id) + self._activity_id = None def set_state(self, name): """ @@ -89,18 +91,6 @@ class Tutorial (object): 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 @@ -112,9 +102,11 @@ class Tutorial (object): #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) - + readfile = addon.create("ReadFile", filename=filename) + if readfile: + self._probeMgr.install(readfile) + #Uninstall now while we have the reference handy + self._probeMgr.uninstall(readfile) class State(object): """ @@ -141,10 +133,9 @@ class State(object): self._actions = action_list or [] - # Unused for now - #self.tests = [] + self._transitions= dict(event_filter_list or []) - self._event_filters = event_filter_list or [] + self._installedEvents = set() self.tutorial = tutorial @@ -168,12 +159,11 @@ class State(object): 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 (event, next_state) in self._transitions.items(): + self._installedEvents.add(self.tutorial.probeManager.subscribe(event, save_args(self._event_filter_state_done_cb, next_state ))) for action in self._actions: - action.do() + self.tutorial.probeManager.install(action) def teardown(self): """ @@ -182,38 +172,37 @@ class State(object): 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() + while len(self._installedEvents) > 0: + self.tutorial.probeManager.unsubscribe(self._installedEvents.pop()) # Undo all the actions related to this state for action in self._actions: - action.undo() + self.tutorial.probeManager.uninstall(action) - def _event_filter_state_done_cb(self, event_filter): + def _event_filter_state_done_cb(self, next_state, event): """ 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 + @param next_state The next state for the transition + @param event The event that occured """ # 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()) + self.tutorial.set_state(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) + Adds an action to the state @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 + self._actions.append(new_action) + return True # remove_action - We did not define names for the action, hence they're # pretty hard to remove on a precise basis @@ -229,19 +218,21 @@ class State(object): Removes all the action associated with this state. A cleared state will not do anything when entered or exited. """ + #FIXME What if the action is currently installed? self._actions = [] - def add_event_filter(self, event_filter): + def add_event_filter(self, event, next_state): """ 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 + @param event The event that will trigger a transition + @param next_state The state to which the transition will lead @return True if added, False otherwise """ - if event_filter not in self._event_filters: - self._event_filters.append(event_filter) + if event not in self._transitions.keys(): + self._transitions[event]=next_state return True return False @@ -249,7 +240,7 @@ class State(object): """ @return The list of event filters associated with this state. """ - return self._event_filters + return self._transitions.items() def clear_event_filters(self): """ @@ -257,12 +248,19 @@ class State(object): was just cleared will become a sink and will be the end of the tutorial. """ - self._event_filters = [] + self._transitions = {} - def is_identical(self, otherState): + def __eq__(self, otherState): """ - Compares two states and tells whether they contain the same states and + Compares two states and tells whether they contain the same states with the + same actions and event filters. + @param otherState The other State that we wish to match + @returns True if every action in this state has a matching action in the + other state with the same properties and values AND if every + event filters in this state has a matching filter in the + other state having the same properties and values AND if both + states have the same name. ` """ if not isinstance(otherState, State): return False @@ -272,27 +270,38 @@ class State(object): # Do they have the same actions? if len(self._actions) != len(otherState._actions): return False + + if len(self._event_filters) != len(otherState._event_filters): + return False + for act in self._actions: found = False + # For each action in the other state, try to match it with this one. for otherAct in otherState._actions: - if act.is_identical(otherAct): + if act == otherAct: found = True break if found == False: + # If we arrive here, then we could not find an action with the + # same values in the other state. We know they're not identical return False # Do they have the same event filters? - if len(self._actions) != len(otherState._actions): - return False for event in self._event_filters: found = False + # For every event filter in the other state, try to match it with + # the current filter. We just need to find one with the right + # properties and values. for otherEvent in otherState._event_filters: - if event.is_identical(otherEvent): + if event == otherEvent: found = True break - if found == False: + if found == False: + # We could not find the given event filter in the other state. return False + # If nothing failed up to now, then every actions and every filters can + # be found in the other state return True class FiniteStateMachine(State): @@ -385,7 +394,7 @@ class FiniteStateMachine(State): self._fsm_setup_done = True # Execute all the FSM level actions for action in self.actions: - action.do() + self.tutorial.probeManager.install(action) # Then, we need to run the setup of the current state self.current_state.setup() @@ -450,7 +459,7 @@ class FiniteStateMachine(State): self._fsm_teardown_done = True # Undo all the FSM level actions here for action in self.actions: - action.undo() + self.tutorial.probeManager.uninstall(action) # TODO : It might be nice to have a start() and stop() method for the # FSM. @@ -506,9 +515,9 @@ class FiniteStateMachine(State): #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) + for event, state in st._transitions: + if state == state_name: + del st._transitions[event] # Remove the state from the dictionary del self._states[state_name] @@ -526,8 +535,8 @@ class FiniteStateMachine(State): next_states = set() - for event_filter in state._event_filters: - next_states.add(event_filter.get_next_state()) + for event, state in state._transitions: + next_states.add(state) return tuple(next_states) @@ -549,9 +558,9 @@ class FiniteStateMachine(State): 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()) + for event, state in st._transitions: + if state == state_name: + states.append(state) continue return tuple(states) @@ -563,42 +572,57 @@ class FiniteStateMachine(State): out_string += st.name + ", " return out_string - def is_identical(self, otherFSM): + def __eq__(self, otherFSM): """ Compares the elements of two FSM to ensure and returns true if they have the same set of states, containing the same actions and the same event filters. - @returns True if the two FSMs have the same content false otherwise + @returns True if the two FSMs have the same content, False otherwise """ if not isinstance(otherFSM, FiniteStateMachine): return False + # Make sure they share the same name if not (self.name == otherFSM.name) or \ not (self.start_state_name == otherFSM.start_state_name): return False - + + # Ensure they have the same number of FSM-level actions if len(self._actions) != len(otherFSM._actions): return False + # Test that we have all the same FSM level actions for act in self._actions: found = False + # For every action in the other FSM, try to match it with the + # current one. for otherAct in otherFSM._actions: - if act.is_identical(otherAct): + if act == otherAct: found = True break if found == False: return False + # Make sure we have the same number of states in both FSMs if len(self._states) != len(otherFSM._states): return False - for state in self._states.itervalues(): - found = False - for otherState in otherFSM._states.itervalues(): - if state.is_identical(otherState): - found = True - break - if found == False: + # For each state, try to find a corresponding state in the other FSM + for state_name in self._states.keys(): + state = self._states[state_name] + other_state = None + try: + # Attempt to use this key in the other FSM. If it's not present + # the dictionary will throw an exception and we'll know we have + # at least one different state in the other FSM + other_state = otherFSM._states[state_name] + except: + return False + # If two states with the same name exist, then we want to make sure + # they are also identical + if not state == other_state: return False + # If we made it here, then all the states in this FSM could be matched to an + # identical state in the other FSM. return True |