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-15 14:23:57 (GMT)
committer Vincent Vinet <vince.vinet@gmail.com>2009-10-15 14:23:57 (GMT)
commitf14bcb9b6c79f7f071e32a7ef9bba5ce440096bd (patch)
tree2f4fd1920e484bfdb567eabe83701cd7306d372d
parentb0274f9a824d8ef82cbe66398d5afaa6ca75d9dc (diff)
Big commit! Everything to make Tutorial execution work through dbus.
-rw-r--r--addons/gtkwidgeteventfilter.py6
-rw-r--r--addons/timerevent.py73
-rw-r--r--tests/probetests.py63
-rwxr-xr-xtests/run-tests.py5
-rw-r--r--tutorius/TProbe.py496
-rw-r--r--tutorius/core.py111
-rw-r--r--tutorius/creator.py6
-rw-r--r--tutorius/dbustools.py24
-rw-r--r--tutorius/engine.py48
-rw-r--r--tutorius/filters.py129
-rw-r--r--tutorius/linear_creator.py3
-rw-r--r--tutorius/properties.py21
-rw-r--r--tutorius/service.py78
-rw-r--r--tutorius/services.py3
14 files changed, 873 insertions, 193 deletions
diff --git a/addons/gtkwidgeteventfilter.py b/addons/gtkwidgeteventfilter.py
index cbfb00c..f5bd0de 100644
--- a/addons/gtkwidgeteventfilter.py
+++ b/addons/gtkwidgeteventfilter.py
@@ -15,6 +15,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from sugar.tutorius.filters import *
from sugar.tutorius.properties import *
+from sugar.tutorius.gtkutils import find_widget
class GtkWidgetEventFilter(EventFilter):
"""
@@ -23,13 +24,12 @@ class GtkWidgetEventFilter(EventFilter):
object_id = TUAMProperty()
event_name = TStringProperty("clicked")
- def __init__(self, next_state=None, object_id=None, event_name=None):
+ def __init__(self, object_id=None, event_name=None):
"""Constructor
- @param next_state default EventFilter param, passed on to EventFilter
@param object_id object fqdn-style identifier
@param event_name event to attach to
"""
- super(GtkWidgetEventFilter,self).__init__(next_state)
+ super(GtkWidgetEventFilter,self).__init__()
self._callback = None
self.object_id = object_id
self.event_name = event_name
diff --git a/addons/timerevent.py b/addons/timerevent.py
new file mode 100644
index 0000000..7b4292c
--- /dev/null
+++ b/addons/timerevent.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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 gobject
+
+from sugar.tutorius.filters import EventFilter
+from sugar.tutorius.properties import TIntProperty
+
+class TimerEvent(EventFilter):
+ """
+ TimerEvent is a special EventFilter that uses gobject
+ timeouts to trigger a state change after a specified amount
+ of time. It must be used inside a gobject main loop to work.
+ """
+ timeout = TIntProperty(15, 0)
+ def __init__(self, timeout=None):
+ """Constructor.
+
+ @param timeout_s timeout in seconds
+ """
+ super(TimerEvent,self).__init__()
+ if timeout:
+ self.timeout = timeout
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install_handlers creates the timer and starts it"""
+ super(TimerEvent,self).install_handlers(callback, **kwargs)
+ #Create the timer
+ self._handler_id = gobject.timeout_add_seconds(self.timeout, self._timeout_cb)
+
+ def remove_handlers(self):
+ """remove handler removes the timer"""
+ super(TimerEvent,self).remove_handlers()
+ if self._handler_id:
+ try:
+ #XXX What happens if this was already triggered?
+ #remove the timer
+ gobject.source_remove(self._handler_id)
+ except:
+ pass
+
+ def _timeout_cb(self):
+ """
+ _timeout_cb triggers the eventfilter callback.
+
+ It is necessary because gobject timers only stop if the callback they
+ trigger returns False
+ """
+ self.do_callback()
+ return False #Stops timeout
+
+__event__ = {
+ "name" : "TimerEvent",
+ "display_name" : "Timer",
+ "icon" : "player_play",
+ "class" : TimerEvent,
+ "mandatory_props" : ["timeout"]
+}
+
diff --git a/tests/probetests.py b/tests/probetests.py
new file mode 100644
index 0000000..a440334
--- /dev/null
+++ b/tests/probetests.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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
+"""
+Probe Tests
+
+"""
+
+import unittest
+import os, sys
+import gtk
+import time
+
+from dbus.mainloop.glib import DBusGMainLoop
+import dbus
+
+from sugar.tutorius.TProbe import TProbe, ProbeProxy
+
+
+class FakeActivity(object):
+ def __init__(self):
+ self.top = gtk.Window(type=gtk.WINDOW_TOPLEVEL)
+ self.top.set_name("Top")
+
+ hbox = gtk.HBox()
+ self.top.add(hbox)
+ hbox.show()
+
+ btn1 = gtk.Button()
+ btn1.set_name("Button1")
+ hbox.pack_start(btn1)
+ btn1.show()
+ self.button = btn1
+
+class ProbeTest(unittest.TestCase):
+ def test_ping(self):
+ m = DBusGMainLoop(set_as_default=True)
+ dbus.set_default_main_loop(m)
+
+ activity = FakeActivity()
+ probe = TProbe("localhost.unittest.ProbeTest", activity.top)
+
+ #Parent, ping the probe
+ proxy = ProbeProxy("localhost.unittest.ProbeTest")
+ res = probe.ping()
+
+ assert res == "alive", "Probe should be alive"
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/tests/run-tests.py b/tests/run-tests.py
index d41aa0a..23d7e24 100755
--- a/tests/run-tests.py
+++ b/tests/run-tests.py
@@ -2,9 +2,9 @@
# This is a dumb script to run tests on the sugar-jhbuild installed files
# The path added is the default path for the jhbuild build
-INSTALL_PATH="../../../../../../install/lib/python2.5/site-packages/"
import os, sys
+INSTALL_PATH=os.path.join(os.path.dirname(__file__),"../../sugar-jhbuild/install/lib/python2.6/site-packages/")
sys.path.insert(0,
os.path.abspath(INSTALL_PATH)
)
@@ -40,6 +40,7 @@ if __name__=='__main__':
import constraintstests
import propertiestests
import serializertests
+ import probetests
suite = unittest.TestSuite()
suite.addTests(unittest.findTestCases(coretests))
suite.addTests(unittest.findTestCases(servicestests))
@@ -52,6 +53,7 @@ if __name__=='__main__':
suite.addTests(unittest.findTestCases(constraintstests))
suite.addTests(unittest.findTestCases(propertiestests))
suite.addTests(unittest.findTestCases(serializertests))
+ suite.addTests(unittest.findTestCases(probetests))
runner = unittest.TextTestRunner()
runner.run(suite)
coverage.stop()
@@ -70,5 +72,6 @@ if __name__=='__main__':
from propertiestests import *
from actiontests import *
from serializertests import *
+ from probetests import *
unittest.main()
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
new file mode 100644
index 0000000..6c0883a
--- /dev/null
+++ b/tutorius/TProbe.py
@@ -0,0 +1,496 @@
+import logging
+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 |
+ -------------------- ----------
+
+"""
+
+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.
+
+ Exposes the following dbus methods:
+ void registered(string service)
+ string ping() -> status
+ string install(string action) -> address
+ void update(string address, string action_props)
+ void uninstall(string address)
+ string subscribe(string pickled_event) -> address
+ void unsubscribe(string address)
+
+ Exposes the following dbus Events:
+ eventOccured(event):
+
+ """
+
+ 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
+ """
+ logging.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid())
+ logging.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default()))
+ logging.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
+ """
+ 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):
+ 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):
+ # 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__
+ suffix = 1
+
+ while self._subscribedEvents.has_key(name+str(suffix)):
+ suffix += 1
+
+ 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.
+
+ Public Methods:
+ ProbeProxy(string activityName) :: Constructor
+ string install(Action action)
+ void update(Action action)
+ void uninstall(Action action)
+ void uninstall_all()
+ string subscribe(Event event, callable callback)
+ void unsubscribe(Event event, callable callback)
+ void unsubscribe_all()
+ """
+ def __init__(self, activityName):
+ """
+ Constructor
+ @param activityName unique activity id
+ """
+ logging.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid())
+ logging.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default()))
+ logging.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))
+ 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 __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
+ """
+ remote_call(self._probe.install, (pickle.dumps(action),),
+ save_args(self.__update_action, action),
+ block=block)
+
+ def update(self, action, block=False):
+ """
+ Update an already installed action's properties and run it again
+ @param action Action to update
+ @return None
+ """
+ if not action in self._actions:
+ raise RuntimeWarning("Action not installed")
+ return
+ remote_call(self._probe.update, (self._actions[action], pickle.dumps(action._props)), block=block)
+
+ def uninstall(self, action, block=False):
+ """
+ Uninstall an installed action
+ @param action Action to uninstall
+ """
+ if action in self._actions:
+ remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block)
+
+ def uninstall_all(self, block=False):
+ """
+ Uninstall all installed actions
+ @return None
+ """
+ for action in self._actions.keys():
+ 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(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):
+ # 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)
+
+ 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 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.
+ """
+ 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")
+ return
+
+ self._probes[activity_id] = ProbeProxy(activity_id)
+ 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.unsubscribe_all()
+ probe.uninstall_all()
+
+ def install(self, action):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].install(action)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def update(self, action):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].update(action)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def uninstall(self, action):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].uninstall(action)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def uninstall_all(self):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].uninstall_all()
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def subscribe(self, event, callback):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].subscribe(event, callback)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def unsubscribe(self, address):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].unsubscribe(address)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def unsubscribe_all(self):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].unsubscribe_all()
+ else:
+ raise RuntimeWarning("No activity attached")
+
diff --git a/tutorius/core.py b/tutorius/core.py
index dd2435e..f51c5fb 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")
@@ -37,7 +35,7 @@ class Tutorial (object):
Tutorial Class, used to run through the FSM.
"""
- def __init__(self, name, fsm,filename= None):
+ def __init__(self, name, fsm, filename=None):
"""
Creates an unattached tutorial.
"""
@@ -51,21 +49,25 @@ 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):
+ probeManager = property(lambda self: self._probeMgr)
+ activityId = property(lambda self: self._activity_id)
+
+ 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,10 @@ class Tutorial (object):
# Uninstall the whole FSM
self.state_machine.teardown()
- #FIXME There should be some amount of resetting done here...
- self.activity = None
-
+ #FIXME (Old) There should be some amount of resetting done here...
+ if not self._activity_id is None:
+ self._probeMgr.detach(self._activity_id)
+ self._activity_id = None
def set_state(self, name):
"""
@@ -90,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
@@ -112,9 +104,10 @@ 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)
+ self._probeMgr.uninstall(readfile)
class State(object):
"""
@@ -144,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
@@ -168,12 +163,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):
"""
@@ -182,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:
- 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
@@ -229,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
@@ -249,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):
"""
@@ -257,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):
"""
@@ -349,7 +346,7 @@ class FiniteStateMachine(State):
self._fsm_setup_done = True
# Execute all the FSM level actions
for action in self.actions:
- action.do()
+ self.tutorial.probeManager.install(action)
# Then, we need to run the setup of the current state
self.current_state.setup()
@@ -414,7 +411,7 @@ class FiniteStateMachine(State):
self._fsm_teardown_done = True
# Undo all the FSM level actions here
for action in self.actions:
- action.undo()
+ self.tutorial.probeManager.uninstall(action)
# TODO : It might be nice to have a start() and stop() method for the
# FSM.
@@ -470,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]
@@ -490,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)
@@ -513,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/creator.py b/tutorius/creator.py
index 513e312..46d4852 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -207,9 +207,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()
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
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 aa8c997..430b708 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,14 @@ class EventFilter(properties.TPropContainer):
Base class for an event filter
"""
- next_state = properties.TStringProperty("None")
-
def __init__(self, next_state=None):
"""
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
@@ -94,111 +73,3 @@ class EventFilter(properties.TPropContainer):
if self._callback:
self._callback(self)
-class TimerEvent(EventFilter):
- """
- TimerEvent is a special EventFilter that uses gobject
- timeouts to trigger a state change after a specified amount
- of time. It must be used inside a gobject main loop to work.
- """
- def __init__(self,next_state,timeout_s):
- """Constructor.
-
- @param next_state default EventFilter param, passed on to EventFilter
- @param timeout_s timeout in seconds
- """
- super(TimerEvent,self).__init__(next_state)
- self._timeout = timeout_s
- self._handler_id = None
-
- def install_handlers(self, callback, **kwargs):
- """install_handlers creates the timer and starts it"""
- super(TimerEvent,self).install_handlers(callback, **kwargs)
- #Create the timer
- self._handler_id = gobject.timeout_add_seconds(self._timeout, self._timeout_cb)
-
- def remove_handlers(self):
- """remove handler removes the timer"""
- super(TimerEvent,self).remove_handlers()
- if self._handler_id:
- try:
- #XXX What happens if this was already triggered?
- #remove the timer
- gobject.source_remove(self._handler_id)
- except:
- pass
-
- def _timeout_cb(self):
- """
- _timeout_cb triggers the eventfilter callback.
-
- It is necessary because gobject timers only stop if the callback they
- trigger returns False
- """
- self.do_callback()
- return False #Stops timeout
-
-class GtkWidgetTypeFilter(EventFilter):
- """
- Event Filter that listens for keystrokes on a widget
- """
- def __init__(self, next_state, object_id, text=None, strokes=None):
- """Constructor
- @param next_state default EventFilter param, passed on to EventFilter
- @param object_id object tree-ish identifier
- @param text resulting text expected
- @param strokes list of strokes expected
-
- At least one of text or strokes must be supplied
- """
- super(GtkWidgetTypeFilter, self).__init__(next_state)
- self._object_id = object_id
- self._text = text
- self._captext = ""
- self._strokes = strokes
- self._capstrokes = []
- self._widget = None
- self._handler_id = None
-
- def install_handlers(self, callback, **kwargs):
- """install handlers
- @param callback default EventFilter callback arg
- """
- super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs)
- logger.debug("~~~GtkWidgetTypeFilter install")
- activity = ObjectStore().activity
- if activity is None:
- logger.error("No activity")
- raise RuntimeWarning("no activity in the objectstore")
-
- self._widget = find_widget(activity, self._object_id)
- if self._widget:
- self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb)
- logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) )
-
- def remove_handlers(self):
- """remove handlers"""
- super(GtkWidgetTypeFilter, self).remove_handlers()
- #if an event was connected, disconnect it
- if self._handler_id:
- self._widget.handler_disconnect(self._handler_id)
- self._handler_id=None
-
- def __keypress_cb(self, widget, event, *args):
- """keypress callback"""
- logger.debug("~~~keypressed!")
- key = event.keyval
- keystr = event.string
- logger.debug("~~~Got key: " + str(key) + ":"+ keystr)
- self._capstrokes += [key]
- #TODO Treat other stuff, such as arrows
- if key == gtk.keysyms.BackSpace:
- self._captext = self._captext[:-1]
- else:
- self._captext = self._captext + keystr
-
- logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext))
- if not self._strokes is None and self._strokes in self._capstrokes:
- self.do_callback()
- if not self._text is None and self._text in self._captext:
- self.do_callback()
-
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/properties.py b/tutorius/properties.py
index abf76e5..b1c6361 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -95,6 +95,27 @@ 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()
+
+ # 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)
+
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
new file mode 100644
index 0000000..c52b7cd
--- /dev/null
+++ b/tutorius/service.py
@@ -0,0 +1,78 @@
+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):
+ remote_call(self._service.launch, (tutorialID, ), block=False)
+
+ def stop(self):
+ remote_call(self._service.stop, (), block=False)
+
+ def pause(self):
+ 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.
"""