Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormike <michael.jmontcalm@gmail.com>2009-10-08 15:33:34 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-10-08 15:33:34 (GMT)
commitf79e0c5fe21474b552cfe62190aa2570c1a94445 (patch)
tree18fbd107559958cdd2c971a0ab09a06a173ba5f1
parent6ef85a852323beaf08ffe89194d388e210e1ce3d (diff)
Merging in LP 429661
-rw-r--r--addons/EmbeddedInterpreter.py30
-rw-r--r--addons/WidgetIdentifier.py35
-rw-r--r--addons/eventgenerator.py60
-rw-r--r--addons/readfile.py55
-rw-r--r--tests/inject.py57
-rw-r--r--tests/probetests.py63
-rwxr-xr-xtests/run-tests.py5
-rw-r--r--tutorius/TProbe.py485
-rw-r--r--tutorius/actions.py1
-rw-r--r--tutorius/constraints.py2
-rw-r--r--tutorius/core.py53
-rw-r--r--tutorius/editor_interpreter.py105
-rw-r--r--tutorius/engine.py39
-rw-r--r--tutorius/events.py36
-rw-r--r--tutorius/ipython_view.py301
-rw-r--r--tutorius/properties.py7
-rw-r--r--tutorius/service.py76
-rw-r--r--tutorius/services.py3
18 files changed, 1385 insertions, 28 deletions
diff --git a/addons/EmbeddedInterpreter.py b/addons/EmbeddedInterpreter.py
new file mode 100644
index 0000000..8c3522e
--- /dev/null
+++ b/addons/EmbeddedInterpreter.py
@@ -0,0 +1,30 @@
+from sugar.tutorius.actions import Action
+from sugar.tutorius.editor_interpreter import EditorInterpreter
+from sugar.tutorius.services import ObjectStore
+
+class EmbeddedInterpreter(Action):
+ def __init__(self):
+ Action.__init__(self)
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = EditorInterpreter(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : "EmbeddedInterpreter",
+ "display_name" : "Embedded Interpreter",
+ "icon" : "message-bubble",
+ "class" : EmbeddedInterpreter,
+ "mandatory_props" : []
+}
diff --git a/addons/WidgetIdentifier.py b/addons/WidgetIdentifier.py
new file mode 100644
index 0000000..3c559b5
--- /dev/null
+++ b/addons/WidgetIdentifier.py
@@ -0,0 +1,35 @@
+from sugar.tutorius.actions import Action
+from sugar.tutorius.editor import WidgetIdentifier as WIPrimitive
+from sugar.tutorius.services import ObjectStore
+
+class WidgetIdentifier(Action):
+ def __init__(self):
+ Action.__init__(self)
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = WIPrimitive(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ # TODO elavoie 2009-07-19
+ # We should disconnect the handlers, however there seems to be an error
+ # saying that the size of the dictionary changed during the iteration
+ # We should investigate this
+ #self._dialog._disconnect_handlers()
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : "WidgetIdentifier",
+ "display_name" : "Widget Identifier",
+ "icon" : "message-bubble",
+ "class" : WidgetIdentifier,
+ "mandatory_props" : []
+}
diff --git a/addons/eventgenerator.py b/addons/eventgenerator.py
new file mode 100644
index 0000000..a91ccf4
--- /dev/null
+++ b/addons/eventgenerator.py
@@ -0,0 +1,60 @@
+# 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
+from sugar.tutorius.actions import *
+from sugar.tutorius.gtkutils import find_widget
+
+class EventGenerator(Action):
+ source = TUAMProperty("")
+ type = TStringProperty("clicked")
+
+ def __init__(self, source=None, type="clicked"):
+ Action.__init__(self)
+
+ if source != None:
+ self.source = source
+
+ if type != "clicked":
+ self.type = type
+
+ def do(self):
+ self._activity = ObjectStore().activity
+
+ # TODO elavoie 2009-07-25 We should eventually use the UAM mecanism
+ widget = find_widget(self._activity, self.source)
+
+
+ # TODO elavoie 2009-07-25 We assume a gtk activity, it might
+ # get messier with other widget systems
+
+ # Call the signal on the widget
+ # We use introspection here to obtain the
+ # method that will send the corresponding
+ # signal on the gtk object
+ getattr(widget, self.type)()
+
+ # That's all!!!
+
+ def undo(self):
+ pass
+
+__action__ = {
+ "name" : "EventGenerator",
+ "display_name" : "Event Generator",
+ "icon" : "message-bubble",
+ "class" : EventGenerator,
+ "mandatory_props" : ["source", "type"]
+}
+
diff --git a/addons/readfile.py b/addons/readfile.py
new file mode 100644
index 0000000..4aa054e
--- /dev/null
+++ b/addons/readfile.py
@@ -0,0 +1,55 @@
+# 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 os
+
+from sugar.tutorius.actions import Action
+from sugar.tutorius.properties import TFileProperty
+from sugar.tutorius.services import ObjectStore
+
+class ReadFile(Action):
+ filename = TFileProperty(None)
+
+ def __init__(self, filename=None):
+ """
+ Calls activity.read_file to load a specified state
+ @param filename Path to the file to read
+ """
+ Action.__init__(self)
+
+ if filename:
+ self.filename=filename
+
+ def do(self):
+ """
+ Perform the action, call read_file on the activity
+ """
+ if os.path.isfile(str(self.filename)):
+ ObjectStore().activity.read_file(self.filename)
+
+ def undo(self):
+ """
+ Not undoable
+ """
+ pass
+
+__action__ = {
+ "name" : "ReadFile",
+ "display_name" : "Read File",
+ "icon" : "message-bubble", #FIXME
+ "class" : ReadFile,
+ "mandatory_props" : ["filename"]
+}
diff --git a/tests/inject.py b/tests/inject.py
new file mode 100644
index 0000000..d69d6ff
--- /dev/null
+++ b/tests/inject.py
@@ -0,0 +1,57 @@
+#Test event injection
+
+import gtk
+import gobject
+import time
+import types
+
+class ClickMaster():
+ def __init__(self):
+ self.event = None
+
+ def connect(self, button):
+ self.id = button.connect("pressed",self.capture_event)
+ self.id2 = button.connect("released",self.capture_event2)
+ self.id3 = button.connect("clicked",self.capture_event3)
+ self.button = button
+
+ def capture_event(self, *args):
+ print "Capture Event"
+ print args
+ self.eventPress = args[-1]
+ return False
+
+ def capture_event2(self, *args):
+ print "Capture Release"
+ print args
+ self.eventReleased = args[-1]
+ return False
+
+ def capture_event3(self, *args):
+ print "Capture Clicked"
+ print args
+ self.eventClicked = args[-1]
+ return False
+
+ def inject_event(self):
+ print "Injecting"
+ print self.event
+ #self.event.put()
+ self.button.emit("button_press_event", self.event)
+
+def print_Event(event):
+ for att in dir(event):
+ if not isinstance(att, types.MethodType):
+ print att, getattr(event, att)
+
+if __name__=='__main__':
+ w = gtk.Window()
+ b = gtk.CheckButton("Auto toggle!")
+ c=ClickMaster()
+ w.add(b)
+ b.show()
+ c.connect(b)
+
+ w.show()
+
+ gtk.main()
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..ec0f9a3
--- /dev/null
+++ b/tutorius/TProbe.py
@@ -0,0 +1,485 @@
+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
+
+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 a Gtk Widget Event
+ @param pickled_event string pickled Event
+ @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
+
+ # 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)
+
+ eventfilter.install_handlers(callback, activity=self._activity)
+
+ name = self._generate_event_reference(event)
+ 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):
+ 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.type
+ 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 = {}
+ 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
+ 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")
+
+ def isAlive(self):
+ try:
+ return self._probe.ping() == "alive"
+ except:
+ return False
+
+ def install(self, action):
+ """
+ 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
+
+ def update(self, action):
+ """
+ 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
+ self._probe.update(self._actions[action], pickle.dumps(action._props))
+
+ def uninstall(self, action):
+ """
+ Uninstall an installed action
+ @param action Action to uninstall
+ """
+ if action in self._actions:
+ self._probe.uninstall(self._actions.pop(action))
+
+ def uninstall_all(self):
+ """
+ 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
+
+ # 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
+
+ # 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 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()
+
+ # 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 unsubscribe_all(self):
+ """
+ Unregister all event listeners
+ @return None
+ """
+ for event, callback in self._events.keys():
+ self.unsubscribe(event, callback)
+
+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, event, callback):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].unsubscribe(event, callback)
+ 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/actions.py b/tutorius/actions.py
index 0db7988..7e0d65e 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -82,6 +82,7 @@ class DragWrapper(object):
"""Callback for end of drag (stolen focus)."""
self._dragging = False
+
def set_draggable(self, value):
"""Setter for the draggable property"""
if bool(value) ^ bool(self._drag_on):
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 36abdfb..2bc27aa 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -201,6 +201,8 @@ class FileConstraint(Constraint):
# TODO : Decide on the architecture for file retrieval on disk
# Relative paths? From where? Support macros?
#
+ 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 41089f1..be2b0ba 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -21,14 +21,11 @@ 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 import addon
logger = logging.getLogger("tutorius")
@@ -37,7 +34,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 +48,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 +78,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):
"""
@@ -112,9 +114,9 @@ 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(self._activity_id, readfile)
class State(object):
"""
@@ -169,11 +171,10 @@ class State(object):
and then triggering the actions.
"""
for eventfilter in self._event_filters:
- eventfilter.install_handlers(self._event_filter_state_done_cb,
- activity=self.tutorial.activity)
+ self.tutorial.probeManager.subscribe(eventfilter, self._event_filter_state_done_cb )
for action in self._actions:
- action.do()
+ self.tutorial.probeManager.install(action)
def teardown(self):
"""
@@ -183,11 +184,11 @@ class State(object):
"""
# Remove the handlers for the all of the state's event filters
for event_filter in self._event_filters:
- event_filter.remove_handlers()
+ self.tutorial.probeManager.unsubscribe(event_filter, self._event_filter_state_done_cb )
# 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):
"""
@@ -385,7 +386,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()
@@ -450,7 +451,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.
diff --git a/tutorius/editor_interpreter.py b/tutorius/editor_interpreter.py
new file mode 100644
index 0000000..d559266
--- /dev/null
+++ b/tutorius/editor_interpreter.py
@@ -0,0 +1,105 @@
+# Copyright (C) 2009, Tutorius.org
+# Greatly influenced by sugar/activity/namingalert.py
+#
+# 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
+""" Tutorial Editor Interpreter Module
+"""
+
+import gtk
+import pango
+from sugar.tutorius.ipython_view import *
+
+from gettext import gettext as _
+
+class EditorInterpreter(gtk.Window):
+ """
+ Interpreter that will be shown to the user
+ """
+ __gtype_name__ = 'TutoriusEditorInterpreter'
+
+ def __init__(self, activity=None):
+ gtk.Window.__init__(self)
+
+ self._activity = activity
+
+ # Set basic properties of window
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(False)
+
+ # Connect to realize signal from ?
+ self.connect('realize', self.__realize_cb)
+
+ # Use an expander widget to allow minimizing
+ self._expander = gtk.Expander(_("Editor Interpreter"))
+ self._expander.set_expanded(True)
+ self.add(self._expander)
+ self._expander.connect("notify::expanded", self.__expander_cb)
+
+
+ # Use the IPython widget to embed
+ self.interpreter = IPythonView()
+ self.interpreter.set_wrap_mode(gtk.WRAP_CHAR)
+
+ # Expose the activity object in the interpreter
+ self.interpreter.updateNamespace({'activity':self._activity})
+
+ # Use a scroll window to permit scrolling of the interpreter prompt
+ swd = gtk.ScrolledWindow()
+ swd.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ swd.add(self.interpreter)
+ self.interpreter.show()
+
+ # Notify GTK that expander is ready
+ self._expander.add(swd)
+ self._expander.show()
+
+ # Notify GTK that the scrolling window is ready
+ swd.show()
+
+ def __expander_cb(self, *args):
+ """Callback for the window expander toggling"""
+ if self._expander.get_expanded():
+ self.__move_expanded()
+ else:
+ self.__move_collapsed()
+
+ def __move_expanded(self):
+ """Move the window to it's expanded position"""
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
+ # leave room for the scrollbar at the right
+ width = swidth - 20
+ height = 200
+
+ self.set_size_request(width, height)
+ # Put at the bottom of the screen
+ self.move(0, sheight-height)
+
+ def __move_collapsed(self):
+ """Move the window to it's collapsed position"""
+ width = 150
+ height = 40
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
+
+ self.set_size_request(width, height)
+ self.move(((swidth-width)/2)-150, sheight-height)
+
+ def __realize_cb(self, widget):
+ """Callback for realize"""
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+ self.__move_expanded()
diff --git a/tutorius/engine.py b/tutorius/engine.py
new file mode 100644
index 0000000..57c08e4
--- /dev/null
+++ b/tutorius/engine.py
@@ -0,0 +1,39 @@
+import dbus.mainloop.glib
+from sugar.tutorius.TProbe import ProbeProxy
+import sugar.tutorius.addon as addon
+
+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)
+ self._probe = ProbeProxy("org.laptop.Calculate")
+ self._bm = 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"
+
+ self._probe.install(self._bm)
+
+
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ self._probe.uninstall(self._bm)
+
+ 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)
+
diff --git a/tutorius/events.py b/tutorius/events.py
new file mode 100644
index 0000000..bf0a8b9
--- /dev/null
+++ b/tutorius/events.py
@@ -0,0 +1,36 @@
+from sugar.tutorius.properties import *
+
+class Event(TPropContainer):
+ source = TUAMProperty()
+ type = TStringProperty("clicked")
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+
+
+ # Providing the hash methods necessary to use events as key
+ # in a dictionary, if new properties are added we should
+ # take them into account here
+ def __hash__(self):
+ return hash(str(self.source) + str(self.type))
+
+ def __eq__(self, e2):
+ return self.source == e2.source and self.type == e2.type
+
+
+ # 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)
+
+
+# Nothing more needs to be added, the additional
+# information is in the object type
+class ClickedEvent(Event):
+ def __init__(self):
+ Event.__init__(self)
+ self.type = "clicked"
+
diff --git a/tutorius/ipython_view.py b/tutorius/ipython_view.py
new file mode 100644
index 0000000..c4294d0
--- /dev/null
+++ b/tutorius/ipython_view.py
@@ -0,0 +1,301 @@
+"""
+Backend to the console plugin.
+
+@author: Eitan Isaacson
+@organization: IBM Corporation
+@copyright: Copyright (c) 2007 IBM Corporation
+@license: BSD
+
+All rights reserved. This program and the accompanying materials are made
+available under the terms of the BSD which accompanies this distribution, and
+is available at U{http://www.opensource.org/licenses/bsd-license.php}
+"""
+# this file is a modified version of source code from the Accerciser project
+# http://live.gnome.org/accerciser
+
+import gtk
+import re
+import sys
+import os
+import pango
+from StringIO import StringIO
+
+try:
+ import IPython
+except Exception,e:
+ raise "Error importing IPython (%s)" % str(e)
+
+ansi_colors = {'0;30': 'Black',
+ '0;31': 'Red',
+ '0;32': 'Green',
+ '0;33': 'Brown',
+ '0;34': 'Blue',
+ '0;35': 'Purple',
+ '0;36': 'Cyan',
+ '0;37': 'LightGray',
+ '1;30': 'DarkGray',
+ '1;31': 'DarkRed',
+ '1;32': 'SeaGreen',
+ '1;33': 'Yellow',
+ '1;34': 'LightBlue',
+ '1;35': 'MediumPurple',
+ '1;36': 'LightCyan',
+ '1;37': 'White'}
+
+class IterableIPShell:
+ def __init__(self,argv=None,user_ns=None,user_global_ns=None,
+ cin=None, cout=None,cerr=None, input_func=None):
+ if input_func:
+ IPython.iplib.raw_input_original = input_func
+ if cin:
+ IPython.Shell.Term.cin = cin
+ if cout:
+ IPython.Shell.Term.cout = cout
+ if cerr:
+ IPython.Shell.Term.cerr = cerr
+
+ if argv is None:
+ argv=[]
+
+ # This is to get rid of the blockage that occurs during
+ # IPython.Shell.InteractiveShell.user_setup()
+ IPython.iplib.raw_input = lambda x: None
+
+ self.term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
+ os.environ['TERM'] = 'dumb'
+ excepthook = sys.excepthook
+ self.IP = IPython.Shell.make_IPython(argv,user_ns=user_ns,
+ user_global_ns=user_global_ns,
+ embedded=True,
+ shell_class=IPython.Shell.InteractiveShell)
+ self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
+ header='IPython system call: ',
+ verbose=self.IP.rc.system_verbose)
+ sys.excepthook = excepthook
+ self.iter_more = 0
+ self.history_level = 0
+ self.complete_sep = re.compile('[\s\{\}\[\]\(\)]')
+
+ def execute(self):
+ self.history_level = 0
+ orig_stdout = sys.stdout
+ sys.stdout = IPython.Shell.Term.cout
+ try:
+ line = self.IP.raw_input(None, self.iter_more)
+ if self.IP.autoindent:
+ self.IP.readline_startup_hook(None)
+ except KeyboardInterrupt:
+ self.IP.write('\nKeyboardInterrupt\n')
+ self.IP.resetbuffer()
+ # keep cache in sync with the prompt counter:
+ self.IP.outputcache.prompt_count -= 1
+
+ if self.IP.autoindent:
+ self.IP.indent_current_nsp = 0
+ self.iter_more = 0
+ except:
+ self.IP.showtraceback()
+ else:
+ self.iter_more = self.IP.push(line)
+ if (self.IP.SyntaxTB.last_syntax_error and
+ self.IP.rc.autoedit_syntax):
+ self.IP.edit_syntax_error()
+ if self.iter_more:
+ self.prompt = str(self.IP.outputcache.prompt2).strip()
+ if self.IP.autoindent:
+ self.IP.readline_startup_hook(self.IP.pre_readline)
+ else:
+ self.prompt = str(self.IP.outputcache.prompt1).strip()
+ sys.stdout = orig_stdout
+
+ def historyBack(self):
+ self.history_level -= 1
+ return self._getHistory()
+
+ def historyForward(self):
+ self.history_level += 1
+ return self._getHistory()
+
+ def _getHistory(self):
+ try:
+ rv = self.IP.user_ns['In'][self.history_level].strip('\n')
+ except IndexError:
+ self.history_level = 0
+ rv = ''
+ return rv
+
+ def updateNamespace(self, ns_dict):
+ self.IP.user_ns.update(ns_dict)
+
+ def complete(self, line):
+ split_line = self.complete_sep.split(line)
+ possibilities = self.IP.complete(split_line[-1])
+ if possibilities:
+ common_prefix = reduce(self._commonPrefix, possibilities)
+ completed = line[:-len(split_line[-1])]+common_prefix
+ else:
+ completed = line
+ return completed, possibilities
+
+ def _commonPrefix(self, str1, str2):
+ for i in range(len(str1)):
+ if not str2.startswith(str1[:i+1]):
+ return str1[:i]
+ return str1
+
+ def shell(self, cmd,verbose=0,debug=0,header=''):
+ stat = 0
+ if verbose or debug: print header+cmd
+ # flush stdout so we don't mangle python's buffering
+ if not debug:
+ input, output = os.popen4(cmd)
+ print output.read()
+ output.close()
+ input.close()
+
+class ConsoleView(gtk.TextView):
+ def __init__(self):
+ gtk.TextView.__init__(self)
+ self.modify_font(pango.FontDescription('Mono'))
+ self.set_cursor_visible(True)
+ self.text_buffer = self.get_buffer()
+ self.mark = self.text_buffer.create_mark('scroll_mark',
+ self.text_buffer.get_end_iter(),
+ False)
+ for code in ansi_colors:
+ self.text_buffer.create_tag(code,
+ foreground=ansi_colors[code],
+ weight=700)
+ self.text_buffer.create_tag('0')
+ self.text_buffer.create_tag('notouch', editable=False)
+ self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
+ self.line_start = \
+ self.text_buffer.create_mark('line_start',
+ self.text_buffer.get_end_iter(), True
+ )
+ self.connect('key-press-event', self._onKeypress)
+ self.last_cursor_pos = 0
+
+ def write(self, text, editable=False):
+ segments = self.color_pat.split(text)
+ segment = segments.pop(0)
+ start_mark = self.text_buffer.create_mark(None,
+ self.text_buffer.get_end_iter(),
+ True)
+ self.text_buffer.insert(self.text_buffer.get_end_iter(), segment)
+
+ if segments:
+ ansi_tags = self.color_pat.findall(text)
+ for tag in ansi_tags:
+ i = segments.index(tag)
+ self.text_buffer.insert_with_tags_by_name(self.text_buffer.get_end_iter(),
+ segments[i+1], tag)
+ segments.pop(i)
+ if not editable:
+ self.text_buffer.apply_tag_by_name('notouch',
+ self.text_buffer.get_iter_at_mark(start_mark),
+ self.text_buffer.get_end_iter())
+ self.text_buffer.delete_mark(start_mark)
+ self.scroll_mark_onscreen(self.mark)
+
+ def showPrompt(self, prompt):
+ self.write(prompt)
+ self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
+
+ def changeLine(self, text):
+ iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ iter.forward_to_line_end()
+ self.text_buffer.delete(self.text_buffer.get_iter_at_mark(self.line_start), iter)
+ self.write(text, True)
+
+ def getCurrentLine(self):
+ rv = self.text_buffer.get_slice(self.text_buffer.get_iter_at_mark(self.line_start),
+ self.text_buffer.get_end_iter(), False)
+ return rv
+
+ def showReturned(self, text):
+ iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ iter.forward_to_line_end()
+ self.text_buffer.apply_tag_by_name('notouch',
+ self.text_buffer.get_iter_at_mark(self.line_start),
+ iter)
+ self.write('\n'+text)
+ if text:
+ self.write('\n')
+ self.showPrompt(self.prompt)
+ self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
+ self.text_buffer.place_cursor(self.text_buffer.get_end_iter())
+
+ def _onKeypress(self, obj, event):
+ if not event.string:
+ return
+ insert_mark = self.text_buffer.get_insert()
+ insert_iter = self.text_buffer.get_iter_at_mark(insert_mark)
+ selection_mark = self.text_buffer.get_selection_bound()
+ selection_iter = self.text_buffer.get_iter_at_mark(selection_mark)
+ start_iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ if start_iter.compare(insert_iter) <= 0 and \
+ start_iter.compare(selection_iter) <= 0:
+ return
+ elif start_iter.compare(insert_iter) > 0 and \
+ start_iter.compare(selection_iter) > 0:
+ self.text_buffer.place_cursor(start_iter)
+ elif insert_iter.compare(selection_iter) < 0:
+ self.text_buffer.move_mark(insert_mark, start_iter)
+ elif insert_iter.compare(selection_iter) > 0:
+ self.text_buffer.move_mark(selection_mark, start_iter)
+
+
+class IPythonView(ConsoleView, IterableIPShell):
+ def __init__(self):
+ ConsoleView.__init__(self)
+ self.cout = StringIO()
+ IterableIPShell.__init__(self, cout=self.cout,cerr=self.cout,
+ input_func=self.raw_input)
+ self.connect('key_press_event', self.keyPress)
+ self.execute()
+ self.cout.truncate(0)
+ self.showPrompt(self.prompt)
+ self.interrupt = False
+
+ def raw_input(self, prompt=''):
+ if self.interrupt:
+ self.interrupt = False
+ raise KeyboardInterrupt
+ return self.getCurrentLine()
+
+ def keyPress(self, widget, event):
+ if event.state & gtk.gdk.CONTROL_MASK and event.keyval == 99:
+ self.interrupt = True
+ self._processLine()
+ return True
+ elif event.keyval == gtk.keysyms.Return:
+ self._processLine()
+ return True
+ elif event.keyval == gtk.keysyms.Up:
+ self.changeLine(self.historyBack())
+ return True
+ elif event.keyval == gtk.keysyms.Down:
+ self.changeLine(self.historyForward())
+ return True
+ elif event.keyval == gtk.keysyms.Tab:
+ if not self.getCurrentLine().strip():
+ return False
+ completed, possibilities = self.complete(self.getCurrentLine())
+ if len(possibilities) > 1:
+ slice = self.getCurrentLine()
+ self.write('\n')
+ for symbol in possibilities:
+ self.write(symbol+'\n')
+ self.showPrompt(self.prompt)
+ self.changeLine(completed or slice)
+ return True
+
+ def _processLine(self):
+ self.history_pos = 0
+ self.execute()
+ rv = self.cout.getvalue()
+ if rv: rv = rv.strip('\n')
+ self.showReturned(rv)
+ self.cout.truncate(0)
+
diff --git a/tutorius/properties.py b/tutorius/properties.py
index 6d30a8d..3a646e4 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -321,7 +321,12 @@ 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..61c6526
--- /dev/null
+++ b/tutorius/service.py
@@ -0,0 +1,76 @@
+from engine import Engine
+import dbus
+
+_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):
+ self._service.launch(tutorialID)
+
+ def stop(self):
+ self._service.stop()
+
+ def pause(self):
+ self._service.pause()
+
+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.
"""