Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVincent Vinet <vince.vinet@gmail.com>2009-10-13 01:28:23 (GMT)
committer Vincent Vinet <vince.vinet@gmail.com>2009-10-13 01:28:23 (GMT)
commit0928acd3d2d845fd6cd28cd848652aedecae0bdb (patch)
tree2150d79acc486cb95abc7818cdcad9ba866d8869
parent53c8fd8df82ba03b4caa84ed4816a80d3c3da0f9 (diff)
run tutorials through the dbus service, currently for calculate only heh
-rw-r--r--tutorius/TProbe.py165
-rw-r--r--tutorius/core.py64
-rw-r--r--tutorius/dbustools.py24
-rw-r--r--tutorius/engine.py36
-rw-r--r--tutorius/linear_creator.py5
-rw-r--r--tutorius/properties.py13
-rw-r--r--tutorius/service.py8
7 files changed, 186 insertions, 129 deletions
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
index ec0f9a3..6c0883a 100644
--- a/tutorius/TProbe.py
+++ b/tutorius/TProbe.py
@@ -10,7 +10,9 @@ 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
"""
@@ -157,38 +159,21 @@ class TProbe(dbus.service.Object):
in_signature='s', out_signature='s')
def subscribe(self, pickled_event):
"""
- Subscribe to a Gtk Widget Event
- @param pickled_event string pickled Event
+ Subscribe to an Event
+ @param pickled_event string pickled EventFilter
@return string unique name of registered event
"""
- event = pickle.loads(str(pickled_event))
-
- # TODO elavoie 2009-07-25 Move to a reference counting implementation
- # to avoid duplicating eventfilters when the event signature is the
- # same
-
- # For now we will assume every probe is inserted in a GTK activity,
- # however, in the future this should be moved in a subclass
- eventfilter = addon.create("GtkWidgetEventFilter")
-
- # There might be a validation of the Address in source in the future
- # and a partial resolution to extract the object_id from the address
- eventfilter.object_id = event.source
-
- # TODO elavoie 2009-07-19
- # There should be a type translation from a tutorius type
- # to a GTK type here
- eventfilter.event_name = event.type
+ 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(event)
+ self.notify(eventfilter)
eventfilter.install_handlers(callback, activity=self._activity)
- name = self._generate_event_reference(event)
+ name = self._generate_event_reference(eventfilter)
self._subscribedEvents[name] = eventfilter
return name
@@ -215,7 +200,14 @@ class TProbe(dbus.service.Object):
# The actual method we will call on the probe to send events
def notify(self, event):
- self.eventOccured(pickle.dumps(event))
+ logging.debug("TProbe :: notify event %s", str(event))
+ #HACK: reinstanciate the event with it's properties, to clear
+ # any internal state from getting pickled
+ if isinstance(event, TPropContainer):
+ newevent = type(event)(**event._props)
+ else:
+ newevent = event
+ self.eventOccured(pickle.dumps(newevent))
# Return a unique name for this action
def _generate_action_reference(self, action):
@@ -232,7 +224,7 @@ class TProbe(dbus.service.Object):
# 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.type
+ name = event.__class__.__name__
suffix = 1
while self._subscribedEvents.has_key(name+str(suffix)):
@@ -270,36 +262,51 @@ class ProbeProxy:
self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface")
self._actions = {}
- self._events = {}
# 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 = {}
- def _handle_signal(pickled_event):
- event = pickle.loads(str(pickled_event))
- if self._registeredCallbacks.has_key(event):
- for callback in self._registeredCallbacks[event].itervalues():
- callback(event)
-
- self._object.connect_to_signal("eventOccured", _handle_signal, dbus_interface="org.tutorius.ProbeInterface")
-
+ 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))
+ logging.debug("ProbeProxy :: Received Event : %s %s", str(event), str(event._props.items()))
+
+ logging.debug("ProbeProxy :: Currently %d events registered", len(self._registeredCallbacks))
+ if self._registeredCallbacks.has_key(event):
+ for callback in self._registeredCallbacks[event].itervalues():
+ callback(event)
+ else:
+ for event in self._registeredCallbacks.keys():
+ logging.debug("==== %s", str(event._props.items()))
+ logging.debug("ProbeProxy :: Event does not appear to be registered")
+
def isAlive(self):
try:
return self._probe.ping() == "alive"
except:
return False
- def install(self, action):
+ def __update_action(self, action, 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
@return None
"""
- address = str(self._probe.install(pickle.dumps(action)))
- self._actions[action] = address
+ remote_call(self._probe.install, (pickle.dumps(action),),
+ save_args(self.__update_action, action),
+ block=block)
- def update(self, action):
+ def update(self, action, block=False):
"""
Update an already installed action's properties and run it again
@param action Action to update
@@ -308,46 +315,32 @@ class ProbeProxy:
if not action in self._actions:
raise RuntimeWarning("Action not installed")
return
- self._probe.update(self._actions[action], pickle.dumps(action._props))
+ remote_call(self._probe.update, (self._actions[action], pickle.dumps(action._props)), block=block)
- def uninstall(self, action):
+ def uninstall(self, action, block=False):
"""
Uninstall an installed action
@param action Action to uninstall
"""
if action in self._actions:
- self._probe.uninstall(self._actions.pop(action))
+ remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block)
- def uninstall_all(self):
+ def uninstall_all(self, block=False):
"""
Uninstall all installed actions
@return None
"""
for action in self._actions.keys():
- self.uninstall(action)
-
- 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
- """
- # 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
- if (event, callback) in self._events:
- raise RuntimeError("event already registered for callback")
- return
+ self.uninstall(action, block)
+ def __update_event(self, event, callback, address):
+ logging.debug("ProbeProxy :: Registered event %s with address %s", str(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(self._probe.subscribe(pickle.dumps(event)))
-
- self._events[(event, callback)] = address
+ address = str(address)
# We use the event object as a key
if not self._registeredCallbacks.has_key(event):
@@ -371,19 +364,7 @@ class ProbeProxy:
return address
- def unsubscribe(self, event, callback):
- """
- Unregister an event listener
- @param address identifier given by subscribe()
- @return None
- """
- if not (event, callback) in self._events:
- raise RuntimeWarning("callback/event not subscribed")
- return
-
- address = self._events.pop((event, callback))
- self._probe.unsubscribe()
-
+ def __clear_event(self, address):
# Cleanup everything
if self._subscribedEvents.has_key(address):
event = self._subscribedEvents[address]
@@ -397,13 +378,43 @@ class ProbeProxy:
self._subscribedEvents.pop(address)
- def unsubscribe_all(self):
+ 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
+ @return address identifier used for unsubscribing
+ """
+ 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=False):
+ """
+ Unregister an event listener
+ @param address identifier given by subscribe()
+ @return None
+ """
+ if address in self._subscribedEvents.keys():
+ remote_call(self._probe.unsubscribe, (address,),
+ return_cb=save_args(self.__clear_event, address),
+ block=block)
+ else:
+ logging.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address)
+
+ def unsubscribe_all(self, block=False):
"""
Unregister all event listeners
@return None
"""
- for event, callback in self._events.keys():
- self.unsubscribe(event, callback)
+ for address in self._subscribedEvents.keys():
+ self.unsubscribe(address, block)
class ProbeManager(object):
"""
@@ -471,9 +482,9 @@ class ProbeManager(object):
else:
raise RuntimeWarning("No activity attached")
- def unsubscribe(self, event, callback):
+ def unsubscribe(self, address):
if self.currentActivity:
- return self._probes[self.currentActivity].unsubscribe(event, callback)
+ return self._probes[self.currentActivity].unsubscribe(address)
else:
raise RuntimeWarning("No activity attached")
diff --git a/tutorius/core.py b/tutorius/core.py
index 8ab0b51..f51c5fb 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -25,6 +25,7 @@ import logging
import os
from sugar.tutorius.TProbe import ProbeManager
+from sugar.tutorius.dbustools import save_args
from sugar.tutorius import addon
logger = logging.getLogger("tutorius")
@@ -92,17 +93,6 @@ class Tutorial (object):
self.state_machine.set_state(name)
- # Currently unused -- equivalent function is in each state
- def _eventfilter_state_done(self, eventfilter):
- """
- Callback handler for eventfilter to notify
- when we must go to the next state.
- """
- #XXX Tests should be run here normally
-
- #Swith to the next state pointed by the eventfilter
- self.set_state(eventfilter.get_next_state())
-
def _prepare_activity(self):
"""
Prepare the activity for the tutorial by loading the saved state and
@@ -116,7 +106,8 @@ class Tutorial (object):
self.activity_init_state_filename
readfile = addon.create("ReadFile", filename=filename)
if readfile:
- self._probeMgr.install(self._activity_id, readfile)
+ self._probeMgr.install(readfile)
+ self._probeMgr.uninstall(readfile)
class State(object):
"""
@@ -146,7 +137,9 @@ class State(object):
# Unused for now
#self.tests = []
- self._event_filters = event_filter_list or []
+ self._transitions= dict(event_filter_list or [])
+
+ self._installedEvents = set()
self.tutorial = tutorial
@@ -170,8 +163,8 @@ class State(object):
Install the state itself, by first registering the event filters
and then triggering the actions.
"""
- for eventfilter in self._event_filters:
- self.tutorial.probeManager.subscribe(eventfilter, self._event_filter_state_done_cb )
+ 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:
self.tutorial.probeManager.install(action)
@@ -183,24 +176,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:
- self.tutorial.probeManager.unsubscribe(event_filter, self._event_filter_state_done_cb )
+ 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:
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
@@ -230,19 +224,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
@@ -250,7 +246,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):
"""
@@ -258,7 +254,7 @@ class State(object):
was just cleared will become a sink and will be the end of the
tutorial.
"""
- self._event_filters = []
+ self._transitions = {}
class FiniteStateMachine(State):
"""
@@ -471,9 +467,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]
@@ -491,8 +487,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)
@@ -514,9 +510,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/dbustools.py b/tutorius/dbustools.py
new file mode 100644
index 0000000..ce28d98
--- /dev/null
+++ b/tutorius/dbustools.py
@@ -0,0 +1,24 @@
+import logging
+
+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):
+ logging.debug("Unhandled asynchronous dbus call response with arguments: %s", str(args))
+
+def logError(error):
+ logging.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:
+ return reply_cb(callable(*args))
+ else:
+ callable(*args, reply_handler=reply_cb, error_handler=errhandler_cb)
+
diff --git a/tutorius/engine.py b/tutorius/engine.py
index 57c08e4..f695de6 100644
--- a/tutorius/engine.py
+++ b/tutorius/engine.py
@@ -1,6 +1,8 @@
+import logging
import dbus.mainloop.glib
-from sugar.tutorius.TProbe import ProbeProxy
-import sugar.tutorius.addon as addon
+from jarabe.model import shell
+
+from sugar.tutorius.bundler import TutorialStore
class Engine:
"""
@@ -10,30 +12,40 @@ class Engine:
def __init__(self):
# FIXME Probe management should be in the probe manager
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
- self._probe = ProbeProxy("org.laptop.Calculate")
- self._bm = None
+ #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._bm == None:
- self._bm = addon.create("BubbleMessage")
- self._bm.position = (300,300)
- self._bm.message = "Tutorial Started"
+ if self._tutorial:
+ self._tutorial.detach()
+ self._tutorial = None
+
+ store = TutorialStore()
+
+ #FIXME Cleanup the handling of 'aliases'
+ activity = self._shell.get_active_activity()
+ self._tutorial = store.load_tutorial(tutorialID, bundle_path=activity.get_bundle_path())
+ self._tutorial.attach("org.laptop.Calculate")
+# if activity in self._activities:
+# self._tutorial.attach(self._activities[activity])
+# else:
+# raise RuntimeError("Current activity alias unknown")
- self._probe.install(self._bm)
def stop(self):
""" Stop the current tutorial
"""
- self._probe.uninstall(self._bm)
+ self._tutorial.detach()
+ self._tutorial = None
def pause(self):
""" Interrupt the current tutorial and save its state in the journal
"""
- self._bm.message = "Tutorial State would be saved"
- self._probe.update(self._bm)
+ raise NotImplementedError("Unable to store tutorial state")
diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py
index 91b11f4..71349ed 100644
--- a/tutorius/linear_creator.py
+++ b/tutorius/linear_creator.py
@@ -45,7 +45,7 @@ class LinearCreator(object):
"""
self.current_actions.append(action)
- def event(self, event_filter):
+ def event(self, event):
"""
Adds a transition to another state. When executing this, all the actions
previously called will be bundled in a single state, with the exit
@@ -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, next_state_name),])
self.state_name = next_state_name
self.nb_state += 1
diff --git a/tutorius/properties.py b/tutorius/properties.py
index d9c68b1..8593e00 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -95,6 +95,19 @@ class TPropContainer(object):
"""
return object.__getattribute__(self, "_props").keys()
+ # Providing the hash methods necessary to use TPropContainers
+ # in a dictionary, according to their properties
+ def __hash__(self):
+ try:
+ #Return a hash of properties (key, value) sorted by key
+ return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0])))))
+ except TypeError:
+ #FIXME For list properties (and maybe others), hashing will fail, fallback to id
+ return id(self)
+
+ def __eq__(self, e2):
+ return self._props.items() == e2._props.items()
+
class TutoriusProperty(object):
"""
The base class for all actions' properties. The interface is the following :
diff --git a/tutorius/service.py b/tutorius/service.py
index 61c6526..c52b7cd 100644
--- a/tutorius/service.py
+++ b/tutorius/service.py
@@ -1,6 +1,8 @@
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"
@@ -57,13 +59,13 @@ class ServiceProxy:
self._service = dbus.Interface(self._object, _DBUS_SERVICE_IFACE)
def launch(self, tutorialID):
- self._service.launch(tutorialID)
+ remote_call(self._service.launch, (tutorialID, ), block=False)
def stop(self):
- self._service.stop()
+ remote_call(self._service.stop, (), block=False)
def pause(self):
- self._service.pause()
+ remote_call(self._service.pause, (), block=False)
if __name__ == "__main__":
import dbus.mainloop.glib