From 3eadc8cee3a83ac98ad526eb11af50d708552e33 Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 26 Feb 2009 15:46:19 +0000 Subject: Merge branch 'tutorial_toolkit' of ssh://mike@bobthebuilder.mine.nu:8080/home/git into tutorial_toolkit Conflicts: source/external/source/sugar-toolkit/src/sugar/tutorius/tutorial.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() diff --git a/src/sugar/tutorius/dialog.py b/src/sugar/tutorius/dialog.py index 298800a..be51a0e 100644 --- a/src/sugar/tutorius/dialog.py +++ b/src/sugar/tutorius/dialog.py @@ -13,11 +13,27 @@ # 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 - +""" +The Dialog module provides means of interacting with the user +through the use of Dialogs. +""" import gtk class TutoriusDialog(gtk.Dialog): + """ + TutoriusDialog is a simple wrapper around gtk.Dialog. + + It allows creating and showing a dialog and connecting the response and + button click events to callbacks. + """ def __init__(self, label="Hint", button_clicked_cb=None, response_cb=None): + """ + Constructor. + + @param label text to be shown on the dialog + @param button_clicked_cb callback for the button click + @param response_cb callback for the dialog response + """ gtk.Dialog.__init__(self) self._button = gtk.Button(label) @@ -34,8 +50,10 @@ class TutoriusDialog(gtk.Dialog): self.set_decorated(False) - def setButtonClickedCallback(self, funct): + def set_button_clicked_cb(self, funct): + """Setter for the button_clicked callback""" self._button.connect("clicked", funct) - def closeSelf(self, Arg=None): + def close_self(self, arg=None): + """Close the dialog""" self.destroy() diff --git a/src/sugar/tutorius/tutorial.py b/src/sugar/tutorius/tutorial.py deleted file mode 100644 index 8c457ae..0000000 --- a/src/sugar/tutorius/tutorial.py +++ /dev/null @@ -1,279 +0,0 @@ -# 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 - - -import gtk -import logging - -from sugar.tutorius.dialog import TutoriusDialog - - -logger = logging.getLogger("tutorius") - -class Event: - def __init__(self, object_name, event_name ): - self.object_name = object_name - self.event_name = event_name - - def test(self, sig, name): - if self.object_name == name and self.event_name == sig: - return True - return False - - -class Tutorial (object): - 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): - object.__init__(self) - self.name = name - self.state_machine = fsm - - self.handlers = [] - self.activity = None - #self.setState("INIT") - #self.state="INIT" - #self.register_signals(self.activity, self.handleEvent, max_depth=10) - - def attach(self, activity): - #For now, absolutely detach if a previous one! - if self.activity: - self.detach() - self.activity = activity - self.state="INIT" - self.register_signals(self.activity,self.handleEvent, max_depth=10) - - def detach(self): - self.disconnectHandlers() - self.activity = None - - def handleEvent(self, *args): - 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): - logger.debug("====NEW STATE: %s====" % next) - self.state = next - dlg = TutoriusDialog(self.state_machine[self.state]["Message"]) - dlg.setButtonClickedCallback(dlg.closeSelf) - dlg.run() - -# @staticmethod -# def logEvent(obj, *args): -# logger.debug("%s" % str(args[-1])) - - def disconnectHandlers(self): - for t, id in self.handlers: - t.disconnect_handler(id) - -# def setState(self,name): -# self.disconnectHandlers() -# self.state = name -# newstate = ABIWORD_MEF.get(name,()) -# for event, n in newstate: -# target = self.activity -# try: -# for obj in event.object_name.split("."): -# target = getattr(target,obj) -# id = target.connect(self.handler,(event.object_name, event.event_name)) -# self.handlers.append(target, id) -# id = target.connect(Tutorial.logEvent,"EVENT %s ON %s" % (event.object_name, event.event_name)) -# self.handlers.append(target, id) -# except Exception, e: -# logger.debug(str(e)) - - def register_signals(self,object,handler,prefix=None,max_depth=None): - """ - Recursive function to register event handlers on an object - 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 object 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 objects - -Handler ID's stored in self.handlers - - @param object the object to recurse on - @param handler the handler function to connect - @param prefix name prepended to the object name to form a chain - @param max_depth maximum recursion depth, None for infinity - """ - #Gtk Containers have a get_children() function - if hasattr(object,"get_children") and \ - hasattr(object.get_children,"__call__"): - for child in object.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, object.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, object.get_name()) \ - if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \ - ) - #register events on the object if a widget XXX necessary to check this? - if isinstance(object,gtk.Widget): - for sig in Tutorial.EVENTS: - try: - self.handlers.append( (object,object.connect(sig,handler,(sig, name) )) ) - except TypeError: - continue - - -############################################################################### -# -# Object oriented model for the FSM -# - -class Action: - """Represents an action to take when entering a state. An action might be - show a dialog to the user, or to play a sound. - - The do() executes the interaction, while the undo() must clean up - everything. """ - def __init__(self): - self.name = "Default Action" - - def do(self): - logging.debug("Doing default action") - - def undo(self): - logging.debug("Undoing default action") - -class DialogAction(Action): - """This is a pop-up dialog that displays a short text to the user.""" - def __init__(self, label, posX, posY): - self.name = "Dialog Action" - self.label = label - self.pos = [posX, posY] - - def do(self): - self.dialog = TutoriusDialog(label) - self.dialog.move(self.pos[0], self,pos[1]) - - def undo(self): - self.dialog.destroy() - -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).""" - self.start_state = start_state - self.actions = setup_actions - - self.tests = [] - - self.current_state = self.start_state - - def setup(self): - for act in self.actions: - act.do() - - def teardown(self): - for act in self.actions: - act.undo - - def verify(self): - return self.current_state.verify() - - -class Executor: - """This is a class that executes a tutorial graph, meaning that it handles - the creation and deletion of states, as well as handling the transitions - between the various states.""" - - def __init__(self): - self.current_state = None - - - def start(self, fsm): - if self.current_state == None: - self.current_state = fsm - - self.current_state.install_handlers() - self.current_state.setup() - - \ No newline at end of file -- cgit v0.9.1