From 5da17d78095187c656002210d937b04cd41f159c Mon Sep 17 00:00:00 2001 From: Vincent Vinet Date: Wed, 25 Feb 2009 21:44:17 +0000 Subject: rename tutorial to core --- (limited to 'src/sugar/tutorius/core.py') diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py new file mode 100644 index 0000000..8919057 --- /dev/null +++ b/src/sugar/tutorius/core.py @@ -0,0 +1,343 @@ +# 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 +""" +Module Tutorial + +This is the main module for Tutorius. + +""" + +import gtk +import logging + +from sugar.tutorius.dialog import TutoriusDialog + + +logger = logging.getLogger("tutorius") + +class Event: + """Event descriptor class. + This class is used to describe events that are expected to happen. + """ + + def __init__(self, object_name, event_name ): + """Constructor for Event + @param object_name str name of the object that will send the event + @param event_name str name of the event + + Example: + evt=Event("0.0.1.1.2", "clicked") + """ + self.object_name = object_name + self.event_name = event_name + + def test(self, sig, name): + """Utility method for testing the equality between a signal and object + names and their expected values. + @param sig str signal name + @param name str object name + @return True if both match the expected values, False otherwise + """ + if self.object_name == name and self.event_name == sig: + return True + return False + + +class Tutorial (object): + """ + Tutorial Class, used to run through the FSM. + """ + EVENTS = [ + "focus", + "button-press-event", + "enter-notify-event", + "leave-notify-event", + "key-press-event", + "text-selected", + "clicked", + ] + + IGNORED_WIDGETS = [ + "GtkVBox", + "GtkHBox", + "GtkAlignment", + "GtkNotebook", + "GtkButton", + "GtkToolItem", + "GtkToolbar", + ] + + def __init__(self, name, fsm): + """Create an unattached tutorial + """ + object.__init__(self) + self.name = name + self.state_machine = fsm + 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.set_state("INIT") + + def detach(self): + """Detach from the current activity""" + self.disconnect_handlers() + self.activity = None + + def handle_event(self, *args): + """Default event handler for the Tutorial. + Tests the received object and signal names onto each defined + transition and changes to the next state if successful. + + The last parameter should be a two-tuple containing the + (signal_name, object_name) + """ + sig, objname = args[-1] + logger.debug("EVENT %s ON %s" % (sig, objname) ) + for transition, next in self.state_machine[self.state]["Events"]: + if transition.test(sig, objname): + self.set_state(next) + +# @staticmethod +# def logEvent(obj, *args): +# logger.debug("%s" % str(args[-1])) + + def disconnect_handlers(self): + """Disconnect all event handlers attached by self""" + #Loop through handlers + for obj, hid in self.handlers: + obj.handler_disconnect(hid) + self.handlers = [] + + + def set_state(self, name): + """Switch to a new state""" + if not self.state_machine.has_key(name): + return + logger.debug("====NEW STATE: %s====" % name) + self.disconnect_handlers() + self.state = name + newstate = self.state_machine.get(name) + for event, unused in newstate["Events"]: + self.register_signal(self.handle_event, \ + event.object_name, event.event_name) + + if newstate.has_key("Message"): + dlg = TutoriusDialog(newstate["Message"]) + dlg.set_button_clicked_cb(dlg.close_self) + dlg.run() + + + def register_signals(self, target, handler, prefix=None, max_depth=None): + """ + Recursive function to register event handlers on an target + and it's children. The event handler is called with an extra + argument which is a two-tuple containing the signal name and + the FQDN-style name of the target that triggered the event. + + This function registers all of the events listed in + Tutorial.EVENTS and omits widgets with a name matching + Tutorial.IGNORED_WIDGETS from the name hierarchy. + + Example arg tuple added: + ("focus", "Activity.Toolbox.Bold") + Side effects: + -Handlers connected on the various targets + -Handler ID's stored in self.handlers + + @param target the target to recurse on + @param handler the handler function to connect + @param prefix name prepended to the target name to form a chain + @param max_depth maximum recursion depth, None for infinity + """ + #Gtk Containers have a get_children() function + if hasattr(target, "get_children") and \ + hasattr(target.get_children, "__call__"): + for child in target.get_children(): + if max_depth is None or max_depth > 0: + #Recurse with a prefix on all children + pre = ".".join( \ + [p for p in (prefix, target.get_name()) \ + if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \ + ) + self.register_signals(child, handler, pre, max_depth-1) + name = ".".join( \ + [p for p in (prefix, target.get_name()) \ + if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \ + ) + #register events on the target if a widget XXX necessary to check this? + if isinstance(target, gtk.Widget): + for sig in Tutorial.EVENTS: + try: + self.handlers.append( \ + (target, target.connect(sig, handler, (sig, name) )) \ + ) + except TypeError: + continue + + def register_signals_numbered(self, \ + target, handler, prefix="0", max_depth=None): + """ + Recursive function to register event handlers on an target + and it's children. The event handler is called with an extra + argument which is a two-tuple containing the signal name and + the FQDN-style name of the target that triggered the event. + + This function registers all of the events listed in + Tutorial.EVENTS + + Example arg tuple added: + ("focus", "1.1.2") + Side effects: + -Handlers connected on the various targets + -Handler ID's stored in self.handlers + + @param target the target to recurse on + @param handler the handler function to connect + @param prefix name prepended to the target name to form a chain + @param max_depth maximum recursion depth, None for infinity + """ + #Gtk Containers have a get_children() function + if hasattr(target, "get_children") and \ + hasattr(target.get_children, "__call__"): + children = target.get_children() + for i in range(len(children)): + child = children[i] + if max_depth is None or max_depth > 0: + #Recurse with a prefix on all children + pre = ".".join( \ + [p for p in (prefix, str(i)) if not p is None] + ) + if max_depth is None: + dep = None + else: + dep = max_depth - 1 + self.register_signals_numbered(child, handler, pre, dep) + #register events on the target if a widget XXX necessary to check this? + if isinstance(target, gtk.Widget): + for sig in Tutorial.EVENTS: + try: + self.handlers.append( \ + (target, target.connect(sig, handler, (sig, prefix) ))\ + ) + except TypeError: + continue + + def register_signal(self, handler, obj_fqdn, signal_name): + """Register a signal handler onto a specific widget + @param handler function to attach as a handler + @param obj_fqdn fqdn-style object name + @param signal_name signal name to connect to + + Side effects: + the object found and the handler id obtained by connect() are + appended in self.handlers + """ + path = obj_fqdn.split(".") + #We select the first object and pop the first zero + obj = self.activity + path.pop(0) + + while len(path) > 0: + obj = obj.get_children()[int(path.pop(0))] + + self.handlers.append( \ + (obj, obj.connect(signal_name, handler, (signal_name, obj_fqdn) ))\ + ) + +class State: + """This is a step in a tutorial. The state represents a collection of + actions to undertake when entering the state, and a description of an + event filter with associated actions to go to the next state.""" + + def __init__(self): + """Initializes the content of the state, as in loading the actions + that are required and building the correct tests.""" + self.actions = [] + self.tests = [] + + + def setup(self): + """Install the state itself. This is the best time to pop-up a dialog + that has to remain for the duration of the state.""" + for act in self.actions: + act.do() + + + def teardown(self): + """Undo every action that was installed for this state. This means + removing dialogs that were displayed, removing highlights, etc...""" + for act in self.actions: + act.undo() + + + 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.""" + def __init__(self, start_state, setup_actions): + """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).""" + State.__init__(self) + + self.start_state = start_state + self.actions = setup_actions + + self.current_state = self.start_state + #TODO Setup current state now? + + def setup(self): + """ + Set up the FSM + """ + for act in self.actions: + act.do() + + def teardown(self): + """ + Revert any changes done by setup() + """ + for act in self.actions: + act.undo() + + def verify(self): + "Verify if the current state passes it's tests""" + return self.current_state.verify() -- cgit v0.9.1