Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tutorius
diff options
context:
space:
mode:
Diffstat (limited to 'tutorius')
-rw-r--r--tutorius/TProbe.py506
-rw-r--r--tutorius/actions.py2
-rw-r--r--tutorius/bundler.py61
-rw-r--r--tutorius/constraints.py5
-rw-r--r--tutorius/core.py104
-rw-r--r--tutorius/creator.py14
-rw-r--r--tutorius/dbustools.py41
-rw-r--r--tutorius/engine.py48
-rw-r--r--tutorius/filters.py24
-rw-r--r--tutorius/linear_creator.py3
-rw-r--r--tutorius/overlayer.py6
-rw-r--r--tutorius/properties.py116
-rw-r--r--tutorius/service.py85
-rw-r--r--tutorius/services.py3
14 files changed, 831 insertions, 187 deletions
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
new file mode 100644
index 0000000..6d7b6e2
--- /dev/null
+++ b/tutorius/TProbe.py
@@ -0,0 +1,506 @@
+import logging
+LOGGER = logging.getLogger("sugar.tutorius.TProbe")
+import os
+
+import gobject
+
+import dbus
+import dbus.service
+import cPickle as pickle
+
+import sugar.tutorius.addon as addon
+
+from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.properties import TPropContainer
+
+from sugar.tutorius.dbustools import remote_call, save_args
+import copy
+
+"""
+ --------------------
+ | ProbeManager |
+ --------------------
+ |
+ V
+ -------------------- ----------
+ | ProbeProxy |<---- DBus ---->| TProbe |
+ -------------------- ----------
+
+"""
+#TODO Add stub error handling for remote calls in the classes so that it will
+# be clearer how errors can be handled in the future.
+
+
+class TProbe(dbus.service.Object):
+ """ Tutorius Probe
+ Defines an entry point for Tutorius into activities that allows
+ performing actions and registering events onto an activity via
+ a DBUS Interface.
+ """
+
+ def __init__(self, activity_name, activity):
+ """
+ Create and register a TProbe for an activity.
+
+ @param activity_name unique activity_id
+ @param activity activity reference, must be a gtk container
+ """
+ LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid())
+ LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default()))
+ LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth()))
+ # Moving the ObjectStore assignment here, in the meantime
+ # the reference to the activity shouldn't be share as a
+ # global variable but passed by the Probe to the objects
+ # that requires it
+ self._activity = activity
+
+ ObjectStore().activity = activity
+
+ self._activity_name = activity_name
+ self._session_bus = dbus.SessionBus()
+
+ # Giving a new name because _name is already used by dbus
+ self._name2 = dbus.service.BusName(activity_name, self._session_bus)
+ dbus.service.Object.__init__(self, self._session_bus, "/tutorius/Probe")
+
+ # Add the dictionary we will use to store which actions and events
+ # are known
+ self._installedActions = {}
+ self._subscribedEvents = {}
+
+ def start(self):
+ """
+ Optional method to call if the probe is not inserted into an
+ existing activity. Starts a gobject mainloop
+ """
+ mainloop = gobject.MainLoop()
+ print "Starting Probe for " + self._activity_name
+ mainloop.run()
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def registered(self, service):
+ print ("Registered with: " + str(service))
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='', out_signature='s')
+ def ping(self):
+ """
+ Allows testing the connection to a Probe
+ @return string "alive"
+ """
+ return "alive"
+
+ # ------------------ Action handling --------------------------------------
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='s')
+ def install(self, pickled_action):
+ """
+ Install an action on the Activity
+ @param pickled_action string pickled action
+ @return string address of installed action
+ """
+ loaded_action = pickle.loads(str(pickled_action))
+ action = addon.create(loaded_action.__class__.__name__)
+
+ address = self._generate_action_reference(action)
+
+ self._installedActions[address] = action
+
+ if action._props:
+ action._props.update(loaded_action._props)
+
+ action.do()
+
+ return address
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='ss', out_signature='')
+ def update(self, address, action_props):
+ """
+ Update an already registered action
+ @param address string address returned by install()
+ @param action_props pickled action properties
+ @return None
+ """
+ action = self._installedActions[address]
+
+ if action._props:
+ props = pickle.loads(str(action_props))
+ action._props.update(props)
+ action.undo()
+ action.do()
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def uninstall(self, address):
+ """
+ Uninstall an action
+ @param address string address returned by install()
+ @return None
+ """
+ if self._installedActions.has_key(address):
+ action = self._installedActions[address]
+ action.undo()
+ self._installedActions.pop(address)
+
+
+ # ------------------ Event handling ---------------------------------------
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='s')
+ def subscribe(self, pickled_event):
+ """
+ Subscribe to an Event
+ @param pickled_event string pickled EventFilter
+ @return string unique name of registered event
+ """
+ #TODO Perform event unmapping once Tutorials use abstract events
+ # instead of concrete EventFilters that are tied to their
+ # implementation.
+ eventfilter = pickle.loads(str(pickled_event))
+
+ # The callback uses the event defined previously and each
+ # successive call to subscribe will register a different
+ # callback that references a different event
+ def callback(*args):
+ self.notify(eventfilter)
+
+ eventfilter.install_handlers(callback, activity=self._activity)
+
+ name = self._generate_event_reference(eventfilter)
+ self._subscribedEvents[name] = eventfilter
+
+ return name
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def unsubscribe(self, address):
+ """
+ Remove subscription to an event
+ @param address string adress returned by subscribe()
+ @return None
+ """
+
+ if self._subscribedEvents.has_key(address):
+ eventfilter = self._subscribedEvents[address]
+ eventfilter.remove_handlers()
+ self._subscribedEvents.pop(address)
+
+ @dbus.service.signal("org.tutorius.ProbeInterface")
+ def eventOccured(self, event):
+ # We need no processing now, the signal will be sent
+ # when the method exit
+ pass
+
+ # The actual method we will call on the probe to send events
+ def notify(self, event):
+ LOGGER.debug("TProbe :: notify event %s", str(event))
+ self.eventOccured(pickle.dumps(event))
+
+ # Return a unique name for this action
+ def _generate_action_reference(self, action):
+ # TODO elavoie 2009-07-25 Should return a universal address
+ name = action.__class__.__name__
+ suffix = 1
+
+ while self._installedActions.has_key(name+str(suffix)):
+ suffix += 1
+
+ return name + str(suffix)
+
+
+ # Return a unique name for this event
+ def _generate_event_reference(self, event):
+ # TODO elavoie 2009-07-25 Should return a universal address
+ name = event.__class__.__name__
+ #Keep the counter to avoid looping all the time
+ suffix = getattr(self, '_event_ref_suffix', 0 ) + 1
+
+ while self._subscribedEvents.has_key(name+str(suffix)):
+ suffix += 1
+
+ #setattr(self, '_event_ref_suffix', suffix)
+
+ return name + str(suffix)
+
+class ProbeProxy:
+ """
+ ProbeProxy is a Proxy class for connecting to a remote TProbe.
+
+ It provides an object interface to the TProbe, which requires pickled
+ strings, across a DBus communication.
+ """
+ def __init__(self, activityName):
+ """
+ Constructor
+ @param activityName unique activity id. Must be a valid dbus bus name.
+ """
+ LOGGER.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid())
+ LOGGER.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default()))
+ LOGGER.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth()))
+ bus = dbus.SessionBus()
+ self._object = bus.get_object(activityName, "/tutorius/Probe")
+ self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface")
+
+ self._actions = {}
+ # We keep those two data structures to be able to have multiple callbacks
+ # for the same event and be able to remove them independently
+ # _subscribedEvents holds a list of callback addresses's for each event
+ # _registeredCallbacks holds the functions to call for each address
+ self._subscribedEvents = {}
+ self._registeredCallbacks = {}
+
+
+ self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface")
+
+ def _handle_signal(self, pickled_event):
+ event = pickle.loads(str(pickled_event))
+ LOGGER.debug("ProbeProxy :: Received Event : %s %s", str(event), str(event._props.items()))
+
+ LOGGER.debug("ProbeProxy :: Currently %d events registered", len(self._registeredCallbacks))
+ if self._registeredCallbacks.has_key(event):
+ for callback in self._registeredCallbacks[event].values():
+ callback(event)
+ else:
+ for event in self._registeredCallbacks.keys():
+ LOGGER.debug("==== %s", str(event._props.items()))
+ LOGGER.debug("ProbeProxy :: Event does not appear to be registered")
+
+ def isAlive(self):
+ try:
+ return self._probe.ping() == "alive"
+ except:
+ return False
+
+ def __update_action(self, action, address):
+ LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address))
+ self._actions[action] = str(address)
+
+ def __clear_action(self, action):
+ self._actions.pop(action, None)
+
+ def install(self, action, block=False):
+ """
+ Install an action on the TProbe's activity
+ @param action Action to install
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ return remote_call(self._probe.install, (pickle.dumps(action),),
+ save_args(self.__update_action, action),
+ block=block)
+
+ def update(self, action, newaction, block=False):
+ """
+ Update an already installed action's properties and run it again
+ @param action Action to update
+ @param newaction Action to update it with
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ #TODO review how to make this work well
+ if not action in self._actions:
+ raise RuntimeWarning("Action not installed")
+ #TODO Check error handling
+ return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block)
+
+ def uninstall(self, action, block=False):
+ """
+ Uninstall an installed action
+ @param action Action to uninstall
+ @param block Force a synchroneous dbus call if True
+ """
+ if action in self._actions:
+ remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block)
+
+ def __update_event(self, event, callback, address):
+ LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address))
+ # Since multiple callbacks could be associated to the same
+ # event signature, we will store multiple callbacks
+ # in a dictionary indexed by the unique address
+ # given for this subscribtion and access this
+ # dictionary from another one indexed by event
+ address = str(address)
+
+ # We use the event object as a key
+ if not self._registeredCallbacks.has_key(event):
+ self._registeredCallbacks[event] = {}
+
+ # TODO elavoie 2009-07-25 decide on a proper exception
+ # taxonomy
+ if self._registeredCallbacks[event].has_key(address):
+ # Oups, how come we have two similar addresses?
+ # send the bad news!
+ raise Exception("Probe subscribe exception, the following address already exists: " + str(address))
+
+ self._registeredCallbacks[event][address] = callback
+
+ # We will keep another dictionary to remember the
+ # event that was associated to this unique address
+ # Let's copy to make sure that even if the event
+ # passed in is modified later it won't screw up
+ # our dictionary (python pass arguments by reference)
+ self._subscribedEvents[address] = copy.copy(event)
+
+ return address
+
+ def __clear_event(self, address):
+ LOGGER.debug("ProbeProxy :: Unregistering adress %s", str(address))
+ # Cleanup everything
+ if self._subscribedEvents.has_key(address):
+ event = self._subscribedEvents[address]
+
+ if self._registeredCallbacks.has_key(event)\
+ and self._registeredCallbacks[event].has_key(address):
+ self._registeredCallbacks[event].pop(address)
+
+ if self._registeredCallbacks[event] == {}:
+ self._registeredCallbacks.pop(event)
+
+ self._subscribedEvents.pop(address)
+ else:
+ LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address)
+
+ def subscribe(self, event, callback, block=True):
+ """
+ Register an event listener
+ @param event Event to listen for
+ @param callback callable that will be called when the event occurs
+ @param block Force a synchroneous dbus call if True (Not allowed yet)
+ @return address identifier used for unsubscribing
+ """
+ LOGGER.debug("ProbeProxy :: Registering event %s", str(hash(event)))
+ if not block:
+ raise RuntimeError("This function does not allow non-blocking mode yet")
+
+ # TODO elavoie 2009-07-25 When we will allow for patterns both
+ # for event types and sources, we will need to revise the lookup
+ # mecanism for which callback function to call
+ return remote_call(self._probe.subscribe, (pickle.dumps(event),),
+ save_args(self.__update_event, event, callback),
+ block=block)
+
+ def unsubscribe(self, address, block=True):
+ """
+ Unregister an event listener
+ @param address identifier given by subscribe()
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address))
+ if not block:
+ raise RuntimeError("This function does not allow non-blocking mode yet")
+ if address in self._subscribedEvents.keys():
+ remote_call(self._probe.unsubscribe, (address,),
+ return_cb=save_args(self.__clear_event, address),
+ block=block)
+ else:
+ LOGGER.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address)
+
+ def detach(self, block=False):
+ """
+ Detach the ProbeProxy from it's TProbe. All installed actions and
+ subscribed events should be removed.
+ """
+ for action in self._actions.keys():
+ self.uninstall(action, block)
+
+ for address in self._subscribedEvents.keys():
+ self.unsubscribe(address, block)
+
+
+class ProbeManager(object):
+ """
+ The ProbeManager provides multiplexing across multiple activity ProbeProxies
+
+ For now, it only handles one at a time, though.
+ Actually it doesn't do much at all. But it keeps your encapsulation happy
+ """
+ def __init__(self):
+ self._probes = {}
+ self._current_activity = None
+
+ def setCurrentActivity(self, activity_id):
+ if not activity_id in self._probes:
+ raise RuntimeError("Activity not attached")
+ self._current_activity = activity_id
+
+ def getCurrentActivity(self):
+ return self._current_activity
+
+ currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
+ def attach(self, activity_id):
+ if activity_id in self._probes:
+ raise RuntimeWarning("Activity already attached")
+
+ self._probes[activity_id] = ProbeProxy(activity_id)
+ #TODO what do we do with this? Raise something?
+ if self._probes[activity_id].isAlive():
+ print "Alive!"
+ else:
+ print "FAil!"
+
+ def detach(self, activity_id):
+ if activity_id in self._probes:
+ probe = self._probes.pop(activity_id)
+ probe.detach()
+
+ def install(self, action, block=False):
+ """
+ Install an action on the current activity
+ @param action Action to install
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].install(action, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def update(self, action, newaction, block=False):
+ """
+ Update an already installed action's properties and run it again
+ @param action Action to update
+ @param newaction Action to update it with
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].update(action, newaction, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def uninstall(self, action, block=False):
+ """
+ Uninstall an installed action
+ @param action Action to uninstall
+ @param block Force a synchroneous dbus call if True
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].uninstall(action, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def subscribe(self, event, callback):
+ """
+ Register an event listener
+ @param event Event to listen for
+ @param callback callable that will be called when the event occurs
+ @return address identifier used for unsubscribing
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].subscribe(event, callback)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def unsubscribe(self, address):
+ """
+ Unregister an event listener
+ @param address identifier given by subscribe()
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].unsubscribe(address)
+ else:
+ raise RuntimeWarning("No activity attached")
+
diff --git a/tutorius/actions.py b/tutorius/actions.py
index cd34976..08f55cd 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -16,6 +16,8 @@
"""
This module defines Actions that can be done and undone on a state
"""
+import gtk
+
from gettext import gettext as _
from sugar.tutorius import addon
diff --git a/tutorius/bundler.py b/tutorius/bundler.py
index 56bbf3e..c9558b1 100644
--- a/tutorius/bundler.py
+++ b/tutorius/bundler.py
@@ -24,6 +24,7 @@ import logging
import os
import uuid
import xml.dom.minidom
+from xml.dom import NotFoundErr
from sugar.tutorius import addon
from sugar.tutorius.core import Tutorial, State, FiniteStateMachine
@@ -37,8 +38,10 @@ def _get_store_root():
return os.path.join(os.getenv("HOME"),
".sugar",profile_name,"tutorius","data")
# this is where activity bundled tutorials should be, under the activity bundle
-def _get_bundle_root():
- return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data")
+def _get_bundle_root(base_path=None):
+ base_path = base_path or os.getenv("SUGAR_BUNDLE_PATH")
+ if base_path:
+ return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data")
INI_ACTIVITY_SECTION = "RELATED_ACTIVITIES"
INI_METADATA_SECTION = "GENERAL_METADATA"
@@ -50,6 +53,7 @@ TUTORIAL_FILENAME = "tutorial.xml"
NODE_COMPONENT = "Component"
NODE_SUBCOMPONENT = "property"
NODE_SUBCOMPONENTLIST = "listproperty"
+NEXT_STATE_ATTR = "next_state"
class TutorialStore(object):
@@ -61,7 +65,7 @@ class TutorialStore(object):
@returns a map of tutorial {names : GUID}.
"""
# check both under the activity data and user installed folders
- paths = [_get_store_root(), _get_bundle_root()]
+ paths = [p for p in [_get_store_root(), _get_bundle_root()] if p ]
tutoGuidName = {}
@@ -89,7 +93,7 @@ class TutorialStore(object):
return tutoGuidName
- def load_tutorial(self, Guid):
+ def load_tutorial(self, Guid, bundle_path=None):
"""
Rebuilds a tutorial object from it's serialized state.
Common storing paths will be scanned.
@@ -97,15 +101,15 @@ class TutorialStore(object):
@param Guid the generic identifier of the tutorial
@returns a Tutorial object containing an FSM
"""
- bundle = TutorialBundler(Guid)
- bundle_path = bundle.get_tutorial_path()
+ bundler = TutorialBundler(Guid, bundle_path=bundle_path)
+ bundler_path = bundler.get_tutorial_path()
config = SafeConfigParser()
- config.read(os.path.join(bundle_path, INI_FILENAME))
+ config.read(os.path.join(bundler_path, INI_FILENAME))
serializer = XMLSerializer()
name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY)
- fsm = serializer.load_fsm(Guid)
+ fsm = serializer.load_fsm(Guid, bundler.Path)
tuto = Tutorial(name, fsm)
return tuto
@@ -125,13 +129,13 @@ class Serializer(object):
exception occur. If no GUID is provided, FSM is written in a new file
in the store root.
"""
- NotImplementedError
+ return NotImplementedError()
def load_fsm(self):
"""
Load fsm from disk.
"""
- NotImplementedError
+ return NotImplementedError()
class XMLSerializer(Serializer):
"""
@@ -270,8 +274,9 @@ class XMLSerializer(Serializer):
Create and return a xml Node from a event filters.
"""
eventFiltersList = doc.createElement("EventFiltersList")
- for event_f in event_filters:
- eventFilterNode = self._create_component_node(event_f, doc)
+ for event, state in event_filters:
+ eventFilterNode = self._create_component_node(event, doc)
+ eventFilterNode.setAttribute(NEXT_STATE_ATTR, str(state))
eventFiltersList.appendChild(eventFilterNode)
return eventFiltersList
@@ -385,17 +390,22 @@ class XMLSerializer(Serializer):
@param filters_elem An XML Element representing a list of event filters
"""
- reformed_event_filters_list = []
+ transition_list = []
event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT)
new_event_filter = None
for event_filter in event_filter_element_list:
+ next_state = event_filter.getAttribute(NEXT_STATE_ATTR)
+ try:
+ event_filter.removeAttribute(NEXT_STATE_ATTR)
+ except NotFoundErr:
+ next_state = None
new_event_filter = self._load_xml_component(event_filter)
if new_event_filter is not None:
- reformed_event_filters_list.append(new_event_filter)
+ transition_list.append((new_event_filter, next_state))
- return reformed_event_filters_list
+ return transition_list
def _load_xml_subcomponents(self, node, properties):
"""
@@ -530,18 +540,18 @@ class XMLSerializer(Serializer):
# Load the event filters
events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0])
- for event in events:
- fsm.add_event_filter(event)
+ for event, next_state in events:
+ fsm.add_event_filter(event, next_state)
return fsm
- def load_fsm(self, guid):
+ def load_fsm(self, guid, path=None):
"""
Load fsm from xml file whose .ini file guid match argument guid.
"""
# Fetch the directory (if any)
- tutorial_dir = self._find_tutorial_dir_with_guid(guid)
+ tutorial_dir = path or self._find_tutorial_dir_with_guid(guid)
# Open the XML file
tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME)
@@ -559,7 +569,7 @@ class TutorialBundler(object):
editor.
"""
- def __init__(self,generated_guid = None):
+ def __init__(self,generated_guid = None, bundle_path=None):
"""
Tutorial_bundler constructor. If a GUID is given in the parameter, the
Tutorial_bundler object will be associated with it. If no GUID is given,
@@ -568,6 +578,7 @@ class TutorialBundler(object):
self.Guid = generated_guid or str(uuid.uuid1())
+ #FIXME: Look for the bundle in the activity first (more specific)
#Look for the file in the path if a uid is supplied
if generated_guid:
#General store
@@ -576,9 +587,13 @@ class TutorialBundler(object):
self.Path = os.path.dirname(store_path)
else:
#Bundle store
- bundle_path = os.path.join(_get_bundle_root(), generated_guid, INI_FILENAME)
- if os.path.isfile(bundle_path):
- self.Path = os.path.dirname(bundle_path)
+ base_bundle_path = _get_bundle_root(bundle_path)
+ if base_bundle_path:
+ bundle_path = os.path.join(base_bundle_path, generated_guid, INI_FILENAME)
+ if os.path.isfile(bundle_path):
+ self.Path = os.path.dirname(bundle_path)
+ else:
+ raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid)
else:
raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid)
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 36abdfb..e91f23a 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -200,7 +200,10 @@ class FileConstraint(Constraint):
def validate(self, value):
# TODO : Decide on the architecture for file retrieval on disk
# Relative paths? From where? Support macros?
- #
+ # FIXME This is a hack to make cases where a default file is not valid
+ # work. It allows None values to be validated, though
+ if value is None:
+ return
if not os.path.isfile(value):
raise FileConstraintError("Non-existing file : %s"%value)
return
diff --git a/tutorius/core.py b/tutorius/core.py
index 4376315..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):
"""
@@ -100,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):
"""
@@ -129,7 +133,9 @@ class State(object):
self._actions = action_list or []
- self._event_filters = event_filter_list or []
+ self._transitions= dict(event_filter_list or [])
+
+ self._installedEvents = set()
self.tutorial = tutorial
@@ -153,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):
"""
@@ -167,24 +172,25 @@ 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
@@ -212,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
@@ -232,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):
"""
@@ -240,7 +248,7 @@ class State(object):
was just cleared will become a sink and will be the end of the
tutorial.
"""
- self._event_filters = []
+ self._transitions = {}
def __eq__(self, otherState):
"""
@@ -385,8 +393,8 @@ class FiniteStateMachine(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()
+ for action in self.actions:
+ self.tutorial.probeManager.install(action)
# Then, we need to run the setup of the current state
self.current_state.setup()
@@ -450,8 +458,8 @@ class FiniteStateMachine(State):
# 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()
+ for action in self.actions:
+ self.tutorial.probeManager.uninstall(action)
# TODO : It might be nice to have a start() and stop() method for the
# FSM.
@@ -507,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]
@@ -527,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)
@@ -550,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)
diff --git a/tutorius/creator.py b/tutorius/creator.py
index 513e312..efa17c3 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -114,7 +114,6 @@ class Creator(object):
"""
self.introspecting = False
eventfilter = addon.create('GtkWidgetEventFilter',
- next_state=None,
object_id=self._selected_widget,
event_name=event_name)
# undo actions so they don't persist through step editing
@@ -207,9 +206,13 @@ class Creator(object):
had_introspect = True
self.introspecting = True
elif isinstance(prop, properties.TStringProperty):
- dlg = TextInputDialog(title="Mandatory property",
+ dlg = TextInputDialog(text="Mandatory property",
field=propname)
setattr(action, propname, dlg.pop())
+ elif isinstance(prop, properties.TIntProperty):
+ dlg = TextInputDialog(text="Mandatory property",
+ field=propname)
+ setattr(action, propname, int(dlg.pop()))
else:
raise NotImplementedError()
@@ -240,7 +243,7 @@ class Creator(object):
Quit editing and cleanup interface artifacts.
"""
self.introspecting = False
- eventfilter = filters.EventFilter(None)
+ eventfilter = filters.EventFilter()
# undo actions so they don't persist through step editing
for action in self._tutorial.current_actions:
action.exit_editmode()
@@ -396,7 +399,10 @@ class EditToolBox(gtk.Window):
def _list_prop_changed(self, widget, evt, action, propname, idx):
try:
- getattr(action, propname)[idx] = int(widget.get_text())
+ #Save props as tuples so that they can be hashed
+ attr = list(getattr(action, propname))
+ attr[idx] = int(widget.get_text())
+ setattr(action, propname, tuple(attr))
except ValueError:
widget.set_text(str(getattr(action, propname)[idx]))
self.__parent._creator._action_refresh_cb(None, None, action)
diff --git a/tutorius/dbustools.py b/tutorius/dbustools.py
new file mode 100644
index 0000000..1b685d7
--- /dev/null
+++ b/tutorius/dbustools.py
@@ -0,0 +1,41 @@
+import logging
+LOGGER = logging.getLogger("sugar.tutorius.dbustools")
+
+def save_args(callable, *xargs, **xkwargs):
+ def __call(*args, **kwargs):
+ kw = dict()
+ kw.update(kwargs)
+ kw.update(xkwargs)
+ return callable(*(xargs+args), **kw)
+ return __call
+
+def ignore(*args):
+ LOGGER.debug("Unhandled asynchronous dbus call response with arguments: %s", str(args))
+
+def logError(error):
+ LOGGER.error("Unhandled asynchronous dbus call error: %s", error)
+
+def remote_call(callable, args, return_cb=None, error_cb=None, block=False):
+ reply_cb = return_cb or ignore
+ errhandler_cb = error_cb or logError
+ if block:
+ try:
+ ret_val = callable(*args)
+ LOGGER.debug("remote_call return arguments: %s", str(ret_val))
+ except Exception, e:
+ #Use the specified error handler even for blocking calls
+ errhandler_cb(e)
+
+ #Return value signature might be :
+ if ret_val is None:
+ #Nothing
+ return reply_cb()
+ elif type(ret_val) in (list, tuple):
+ #Several parameters
+ return reply_cb(*ret_val)
+ else:
+ #One parameter
+ return reply_cb(ret_val)
+ else:
+ callable(*args, reply_handler=reply_cb, error_handler=errhandler_cb)
+
diff --git a/tutorius/engine.py b/tutorius/engine.py
new file mode 100644
index 0000000..dda9f3f
--- /dev/null
+++ b/tutorius/engine.py
@@ -0,0 +1,48 @@
+import logging
+import dbus.mainloop.glib
+from jarabe.model import shell
+
+from sugar.tutorius.bundler import TutorialStore
+from sugar.bundle.activitybundle import ActivityBundle
+
+class Engine:
+ """
+ Driver for the execution of tutorials
+ """
+
+ def __init__(self):
+ # FIXME Probe management should be in the probe manager
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ #FIXME shell.get_model() will only be useful in the shell process
+ self._shell = shell.get_model()
+ self._tutorial = None
+
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ if self._tutorial:
+ self._tutorial.detach()
+ self._tutorial = None
+
+ store = TutorialStore()
+
+ #Get the active activity from the shell
+ activity = self._shell.get_active_activity()
+ self._tutorial = store.load_tutorial(tutorialID, bundle_path=activity.get_bundle_path())
+
+ #TProbes automatically use the bundle id, available from the ActivityBundle
+ bundle = ActivityBundle(activity.get_bundle_path())
+ self._tutorial.attach(bundle.get_bundle_id())
+
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ self._tutorial.detach()
+ self._tutorial = None
+
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ raise NotImplementedError("Unable to store tutorial state")
+
diff --git a/tutorius/filters.py b/tutorius/filters.py
index 0055763..44621d5 100644
--- a/tutorius/filters.py
+++ b/tutorius/filters.py
@@ -15,13 +15,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-import gobject
-import gtk
import logging
logger = logging.getLogger("filters")
-from sugar.tutorius.gtkutils import find_widget
-from sugar.tutorius.services import ObjectStore
from sugar.tutorius import properties
@@ -30,31 +26,13 @@ class EventFilter(properties.TPropContainer):
Base class for an event filter
"""
- next_state = properties.TStringProperty("None")
-
- def __init__(self, next_state=None):
+ def __init__(self):
"""
Constructor.
- @param next_state name of the next state
"""
super(EventFilter, self).__init__()
- if next_state:
- self.next_state = next_state
self._callback = None
- def get_next_state(self):
- """
- Getter for the next state
- """
- return self.next_state
-
- def set_next_state(self, new_next_name):
- """
- Setter for the next state. Should only be used during construction of
- the event_fitler, not while the tutorial is running.
- """
- self.next_state = new_next_name
-
def install_handlers(self, callback, **kwargs):
"""
install_handlers is called for eventfilters to setup all
diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py
index 91b11f4..78e94ce 100644
--- a/tutorius/linear_creator.py
+++ b/tutorius/linear_creator.py
@@ -58,9 +58,8 @@ class LinearCreator(object):
# Set the next state name - there is no way the caller should have
# to deal with that.
next_state_name = "State %d" % (self.nb_state+1)
- event_filter.set_next_state(next_state_name)
state = State(self.state_name, action_list=self.current_actions,
- event_filter_list=[event_filter])
+ event_filter_list=[(event_filter, next_state_name),])
self.state_name = next_state_name
self.nb_state += 1
diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py
index 6b1b948..0a3d542 100644
--- a/tutorius/overlayer.py
+++ b/tutorius/overlayer.py
@@ -157,7 +157,7 @@ class TextBubble(gtk.Widget):
A CanvasDrawableWidget drawing a round textbox and a tail pointing
to a specified widget.
"""
- def __init__(self, text, speaker=None, tailpos=[0,0]):
+ def __init__(self, text, speaker=None, tailpos=(0,0)):
"""
Creates a new cairo rendered text bubble.
@@ -199,7 +199,7 @@ class TextBubble(gtk.Widget):
# TODO fetch speaker coordinates
# draw bubble tail if present
- if self.tailpos != [0,0]:
+ if self.tailpos != (0,0):
context.move_to(xradius-width/4, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
context.line_to(xradius+width/4, yradius)
@@ -228,7 +228,7 @@ class TextBubble(gtk.Widget):
context.fill()
# bubble painting. Redrawing the inside after the tail will combine
- if self.tailpos != [0,0]:
+ if self.tailpos != (0,0):
context.move_to(xradius-width/4, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
context.line_to(xradius+width/4, yradius)
diff --git a/tutorius/properties.py b/tutorius/properties.py
index 4c34511..78e3c2b 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -95,89 +95,25 @@ class TPropContainer(object):
"""
return object.__getattribute__(self, "_props").keys()
- def __eq__(self, otherContainer):
- """
- Compare this property container to the other one and returns True only if
- the every property of the first one can be found in the other container, with
- the same name and the same value.
-
- @param otherContainer The other container that we wish to test for equality.
- @returns True if every property in the first container can be found with the same
- value and the same name in the second container.
- """
- # Make sure both have the same number of properties
- if len(self._props) != len(otherContainer._props):
- return False
+ # Providing the hash methods necessary to use TPropContainers
+ # in a dictionary, according to their properties
+ def __hash__(self):
+ #Return a hash of properties (key, value) sorted by key
+ #We need to transform the list of property key, value lists into
+ # a tuple of key, value tuples
+ return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0])))))
- if not(type(self) == type(otherContainer)):
- return False
+ def __eq__(self, e2):
+ return self._props == e2._props
- # For every property in this container
- for prop in self._props.keys():
- found = False
- # Try to match it with another property present in the other container
- for otherProp in otherContainer._props.keys():
- # If we were able to match the name, then we look up the value
- if prop == otherProp:
- this_type = getattr(type(self), prop).type
- other_type = getattr(type(otherContainer), prop).type
- if this_type != other_type:
- return False
-
- # If this is an addon list, then we need to make sure that
- # every element of the list is also present in the other list
- if this_type == "addonlist":
- if not self._are_lists_identical(self._props[prop], otherContainer._props[prop]):
- return False
- found = True
- break
+ # Adding methods for pickling and unpickling an object with
+ # properties
+ def __getstate__(self):
+ return self._props.copy()
+
+ def __setstate__(self, dict):
+ self._props.update(dict)
- # If this is just an embedded / decorated container, then we want to
- # make sure the sub component are identical.
- elif this_type == "addon":
- if not (self._props[prop] == otherContainer._props[prop]):
- return False
- found = True
- break
- else:
- if self._props[prop] == otherContainer._props[prop]:
- found = True
- break
- # If we arrive here, then we couldn't find any property in the second
- # container that matched the current one. We know that the two containers are
- # not equal.
- if found == False:
- return False
- return True
-
- def _are_lists_identical(self, myList, otherList):
- """
- Compares two lists of property containers to see if they are identical (
- they have the same properties
-
- @param myList The first list of properties containers
- @param otherList The second list of properties containers
- @return True if all of the properties inside the list are identical. False otherwise.
- """
- # For each property in the first list,
- for container in myList:
- found = False
- # Attempt to match it with every property in the other list
- for other_container in otherList:
- # If the containers are identical,
- if container == other_container:
- # We found a matching container. We don't need to search in the
- # second list anymore, so we break
- found = True
- break
- # In the case the property was not found inside the second list
- if found == False:
- # We know right away that the two lists are not identical
- return False
- # If we were able to match each property in the first list, then we
- # can say the lists are equal.
- return True
-
class TutoriusProperty(object):
"""
The base class for all actions' properties. The interface is the following :
@@ -277,8 +213,20 @@ class TArrayProperty(TutoriusProperty):
self.type = "array"
self.max_size_limit = MaxSizeConstraint(max_size_limit)
self.min_size_limit = MinSizeConstraint(min_size_limit)
- self.default = self.validate(value)
+ self.default = tuple(self.validate(value))
+ #Make this thing hashable
+ def __setstate__(self, state):
+ self.max_size_limit = MaxSizeConstraint(state["max_size_limit"])
+ self.min_size_limit = MinSizeConstraint(state["min_size_limit"])
+ self.value = state["value"]
+
+ def __getstate__(self):
+ return dict(
+ max_size_limit=self.max_size_limit.limit,
+ min_size_limit=self.min_size_limit.limit,
+ value=self.value,
+ )
class TColorProperty(TutoriusProperty):
"""
Represents a RGB color with 3 8-bit integer values.
@@ -357,8 +305,10 @@ class TUAMProperty(TutoriusProperty):
"""
Represents a widget of the interface by storing its UAM.
"""
- # TODO : Pending UAM check-in (LP 355199)
- pass
+ def __init__(self, value=None):
+ TutoriusProperty.__init__(self)
+
+ self.type = "uam"
class TAddonProperty(TutoriusProperty):
"""
diff --git a/tutorius/service.py b/tutorius/service.py
new file mode 100644
index 0000000..21f0cf1
--- /dev/null
+++ b/tutorius/service.py
@@ -0,0 +1,85 @@
+from engine import Engine
+import dbus
+
+from dbustools import remote_call
+
+_DBUS_SERVICE = "org.tutorius.Service"
+_DBUS_PATH = "/org/tutorius/Service"
+_DBUS_SERVICE_IFACE = "org.tutorius.Service"
+
+class Service(dbus.service.Object):
+ """
+ Global tutorius entry point to control the whole system
+ """
+
+ def __init__(self):
+ bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, _DBUS_PATH)
+
+ self._engine = None
+
+ def start(self):
+ """ Start the service itself
+ """
+ # For the moment there is nothing to do
+ pass
+
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="s", out_signature="")
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ if self._engine == None:
+ self._engine = Engine()
+ self._engine.launch(tutorialID)
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="", out_signature="")
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ self._engine.stop()
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="", out_signature="")
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ self._engine.pause()
+
+class ServiceProxy:
+ """ Proxy to connect to the Service object, abstracting the DBus interface"""
+
+ def __init__(self):
+ bus = dbus.SessionBus()
+ self._object = bus.get_object(_DBUS_SERVICE,_DBUS_PATH)
+ self._service = dbus.Interface(self._object, _DBUS_SERVICE_IFACE)
+
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ remote_call(self._service.launch, (tutorialID, ), block=False)
+
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ remote_call(self._service.stop, (), block=False)
+
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ remote_call(self._service.pause, (), block=False)
+
+if __name__ == "__main__":
+ import dbus.mainloop.glib
+ import gobject
+
+ loop = gobject.MainLoop()
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ s = Service()
+ loop.run()
+
diff --git a/tutorius/services.py b/tutorius/services.py
index 9ed2e50..e7b17d8 100644
--- a/tutorius/services.py
+++ b/tutorius/services.py
@@ -22,6 +22,9 @@ This module supplies services to be used by States, FSMs, Actions and Filters.
Services provided are:
-Access to the running activity
-Access to the running tutorial
+
+TODO: Passing the activity reference should be done by the Probe instead
+of being a global variable.
"""