From 0c3f127c86af818d260966d2292b199757087157 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Sat, 11 Jul 2009 21:39:46 +0000 Subject: repackage --- (limited to 'tutorius/core.py') 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 +# +# 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 -- cgit v0.9.1