Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--addons/bubblemessagewimg.py119
-rw-r--r--tests/addontests.py7
-rw-r--r--tests/constraintstests.py42
-rw-r--r--tests/enginetests.py182
-rw-r--r--tests/linear_creatortests.py79
-rw-r--r--tests/probetests.py84
-rw-r--r--tests/propertiestests.py56
-rw-r--r--tests/translatortests.py131
-rw-r--r--tutorius/TProbe.py162
-rw-r--r--tutorius/addon.py4
-rw-r--r--tutorius/constraints.py39
-rw-r--r--tutorius/creator.py96
-rw-r--r--tutorius/dbustools.py16
-rw-r--r--tutorius/engine.py265
-rw-r--r--tutorius/linear_creator.py94
-rw-r--r--tutorius/overlayer.py196
-rw-r--r--tutorius/properties.py50
-rw-r--r--tutorius/translator.py200
-rw-r--r--tutorius/vault.py27
19 files changed, 1446 insertions, 403 deletions
diff --git a/addons/bubblemessagewimg.py b/addons/bubblemessagewimg.py
new file mode 100644
index 0000000..9c3dfc1
--- /dev/null
+++ b/addons/bubblemessagewimg.py
@@ -0,0 +1,119 @@
+# 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 Action, DragWrapper
+from sugar.tutorius.properties import TStringProperty, TResourceProperty, TArrayProperty
+from sugar.tutorius import overlayer
+from sugar.tutorius.services import ObjectStore
+
+class BubbleMessageWImg(Action):
+ message = TStringProperty("Message")
+ # Create the position as an array of fixed-size 2
+ position = TArrayProperty((0,0), 2, 2)
+ # Do the same for the tail position
+ tail_pos = TArrayProperty((0,0), 2, 2)
+ imgpath = TResourceProperty("")
+
+ def __init__(self, message=None, position=None, speaker=None, tail_pos=None, imgpath=None):
+ """
+ Shows a dialog with a given text, at the given position on the screen.
+
+ @param message A string to display to the user
+ @param position A list of the form [x, y]
+ @param speaker treeish representation of the speaking widget
+ @param tail_pos The position of the tail of the bubble; useful to point to
+ specific elements of the interface
+ """
+ Action.__init__(self)
+
+ if position:
+ self.position = position
+ if tail_pos:
+ self.tail_pos = tail_pos
+ if message:
+ self.message = message
+ if imgpath:
+ self.imgpath = imgpath
+
+ self.overlay = None
+ self._bubble = None
+ self._speaker = None
+
+ def do(self, **kwargs):
+ """
+ Show the dialog
+ """
+ # get or inject overlayer
+ self.overlay = ObjectStore().activity._overlayer
+ # FIXME: subwindows, are left to overlap this. This behaviour is
+ # undesirable. subwindows (i.e. child of top level windows) should be
+ # handled either by rendering over them, or by finding different way to
+ # draw the overlay.
+
+ if not self.overlay:
+ self.overlay = ObjectStore().activity._overlayer
+ if not self._bubble:
+ x, y = self.position
+ # TODO: tails are relative to tailpos. They should be relative to
+ # the speaking widget. Same of the bubble position.
+ self._bubble = overlayer.TextBubbleWImg(text=self.message,
+ tailpos=self.tail_pos,imagepath=self.imgpath.default)
+ self._bubble.show()
+ self.overlay.put(self._bubble, x, y)
+ self.overlay.queue_draw()
+
+ def undo(self):
+ """
+ Destroy the dialog
+ """
+ if self._bubble:
+ self._bubble.destroy()
+ self._bubble = None
+
+ def enter_editmode(self, *args):
+ """
+ Enters edit mode. The action should display itself in some way,
+ without affecting the currently running application.
+ """
+ if not self.overlay:
+ self.overlay = ObjectStore().activity._overlayer
+ assert not self._drag, "bubble action set to editmode twice"
+ x, y = self.position
+ self._bubble = overlayer.TextBubbleWImg(text=self.message,
+ tailpos=self.tail_pos,imagepath=self.imgpath)
+ self.overlay.put(self._bubble, x, y)
+ self._bubble.show()
+
+ self._drag = DragWrapper(self._bubble, self.position, True)
+
+ def exit_editmode(self, *args):
+ x,y = self._drag.position
+ self.position = (int(x), int(y))
+ if self._drag:
+ self._drag.draggable = False
+ self._drag = None
+ if self._bubble:
+ self.overlay.remove(self._bubble)
+ self._bubble = None
+ self.overlay = None
+
+__action__ = {
+ "name" : "BubbleMessageWImg",
+ "display_name" : "Message Bubble with image",
+ "icon" : "message-bubble",
+ "class" : BubbleMessageWImg,
+ "mandatory_props" : ["message",'imgpath']
+}
+
diff --git a/tests/addontests.py b/tests/addontests.py
index ceaee2b..5a48e42 100644
--- a/tests/addontests.py
+++ b/tests/addontests.py
@@ -47,4 +47,9 @@ class AddonTest(unittest.TestCase):
def test_get_addon_meta(self):
addon._cache = None
meta = addon.get_addon_meta("BubbleMessage")
- assert set(meta.keys()) == set(['type', 'mandatory_props', 'class', 'display_name', 'name', 'icon',])
+ keys = meta.keys()
+ assert 'mandatory_props' in keys
+ assert 'class' in keys
+ assert 'display_name' in keys
+ assert 'name' in keys
+ assert 'icon' in keys
diff --git a/tests/constraintstests.py b/tests/constraintstests.py
index 4e19a92..a5ccf26 100644
--- a/tests/constraintstests.py
+++ b/tests/constraintstests.py
@@ -240,5 +240,47 @@ class FileConstraintTest(unittest.TestCase):
except FileConstraintError:
pass
+class ResourceConstraintTest(unittest.TestCase):
+ def test_valid_names(self):
+ name1 = "file_" + unicode(uuid.uuid1()) + ".png"
+ name2 = unicode(uuid.uuid1()) + "_" + unicode(uuid.uuid1()) + ".extension"
+ name3 = "/home/user/.sugar/_random/new_image1231_" + unicode(uuid.uuid1()).upper() + ".mp3"
+ name4 = "a_" + unicode(uuid.uuid1())
+ name5 = ""
+
+ cons = ResourceConstraint()
+
+ # All of those names should pass without exceptions
+ cons.validate(name1)
+ cons.validate(name2)
+ cons.validate(name3)
+ cons.validate(name4)
+ cons.validate(name5)
+
+ def test_invalid_names(self):
+ bad_name1 = ".jpg"
+ bad_name2 = "_.jpg"
+ bad_name3 = "_" + unicode(uuid.uuid1())
+
+ cons = ResourceConstraint()
+
+ try:
+ cons.validate(bad_name1)
+ assert False, "%s should not be a valid resource name" % bad_name1
+ except ResourceConstraintError:
+ pass
+
+ try:
+ cons.validate(bad_name2)
+ assert False, "%s should not be a valid resource name" % bad_name2
+ except ResourceConstraintError:
+ pass
+
+ try:
+ cons.validate(bad_name3)
+ assert False, "%s should not be a valid resource name" % bad_name3
+ except ResourceConstraintError:
+ pass
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/enginetests.py b/tests/enginetests.py
index 30d68de..1723954 100644
--- a/tests/enginetests.py
+++ b/tests/enginetests.py
@@ -25,37 +25,98 @@ and event filters. Those are in their separate test module
"""
import unittest
+from functools import partial
+from uuid import uuid1
from sugar.tutorius.tutorial import Tutorial
from sugar.tutorius.engine import TutorialRunner
+import sugar.tutorius.engine as engine
+from sugar.tutorius.actions import Action
from sugar.tutorius.filters import EventFilter
from actiontests import CountAction
+class MockProbeMgrMultiAddons(object):
+ def __init__(self):
+ self.action_dict = {}
+ self.event_dict = {}
+ self.event_cb_dict = {}
+
+ self._action_installed_cb_list = []
+ self._install_error_cb_list = []
+ self._event_subscribed_cb_list = []
+ self._subscribe_error_cb_list = []
+
+ currentActivity = property(fget=lambda s:s, fset=lambda s, v: v)
+
+ def run_install_cb(self, action_number, action):
+ self._action_installed_cb_list[action_number](str(uuid1()))
+
+ def run_install_error_cb(self, action_number):
+ self._install_error_cb_list[action_number](Exception("Could not install action..."))
+
+ def run_subscribe_cb(self, event_number):
+ self._event_subscribed_cb_list[event_number](str(uuid1()))
+
+ def run_subscribe_error(self, event_number):
+ self._subscribe_error_cb_list[event_number](Exception("Could not subscribe to event"))
+
+ def install(self, action, action_installed_cb, error_cb):
+ action_address = str(uuid1())
+ self.action_dict[action_address] = action
+ self._action_installed_cb_list.append(action_installed_cb)
+ self._install_error_cb_list.append(error_cb)
+
+ def update(self, action_address, new_action):
+ self.action_dict[action_address] = new_action
+
+ def uninstall(self, action_address):
+ del self.action_dict[action_address]
+
+ def subscribe(self, event, notif_cb, subscribe_cb, error_cb):
+ event_address = str(uuid1())
+ self.event_dict[event_address] = event
+ self.event_cb_dict[event_address] = notif_cb
+ self._event_subscribed_cb_list.append(subscribe_cb)
+ self._subscribe_error_cb_list.append(error_cb)
+
+ def unsubscribe(self, address):
+ for (event_address, other_event) in self.event_dict.values():
+ if event == other_event:
+ del self.event_dict[address]
+ break
class MockProbeMgr(object):
def __init__(self):
self.action = None
self.event = None
self.cB = None
+
+ self._action_installed_cb = None
+ self._install_error_cb = None
def doCB(self):
self.cB(self.event)
currentActivity = property(fget=lambda s:s, fset=lambda s, v: v)
- def install(self, action, block=False):
+ def install(self, action, action_installed_cb, error_cb):
self.action = action
+ self._action_installed_cb = action_installed_cb
+ self._install_error_cb = partial(error_cb, action)
- def update(self, action, newaction, block=False):
+ def update(self, action_address, newaction):
self.action = newaction
- def uninstall(self, action, block=False):
+ def uninstall(self, action_address):
self.action = None
- def subscribe(self, event, callback):
+ def subscribe(self, event, notif_cb, event_sub_cb, error_cb):
self.event = event
- self.cB = callback
- self.event.install_handlers(callback)
+ self.cB = notif_cb
+ self.event.install_handlers(notif_cb)
+ # Save the callbacks for this action
+ self.event_sub_cB = event_sub_cb
+ self._subscribe_error_cb = error_cb
return str(event)
def unsubscribe(self, address):
@@ -63,9 +124,104 @@ class MockProbeMgr(object):
class MockEvent(EventFilter):
pass
+
+class TestRunnerStates(unittest.TestCase):
+ def setUp(self):
+ self.pM = MockProbeMgr()
+ self.tutorial = Tutorial("TutorialRunner")
+ self.state_name = self.tutorial.add_state()
+ self.tutorial.update_transition(Tutorial.INITIAL_TRANSITION_NAME,
+ None, self.state_name)
+ self.action = CountAction()
+ self.tutorial.add_action(self.state_name, self.action)
+ self.event = MockEvent()
+ self.tutorial.add_transition(self.state_name, (self.event, Tutorial.END))
+
+ self.runner = TutorialRunner(self.tutorial, self.pM)
+
+ def test_setup_states(self):
+ assert self.runner._runner_state == engine.RUNNER_STATE_IDLE, "Idle should be the initial state for the runner"
+
+ self.runner.start()
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Setup Actions State should be entered after start"
+ self.pM._action_installed_cb('action1')
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "State should be Setup Events after all actions are installed"
+
+ self.pM.event_sub_cB('event1')
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_AWAITING_NOTIFICATIONS, "State should be Awaiting Notifications once all events are installed"
+
+ def test_setup_actions_errors(self):
+ self.runner.start()
+
+ self.pM._install_error_cb(Exception("Fake Exception"))
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Setup Events should be reached after error on action installation"
+
+ self.pM._subscribe_error_cb(Exception("Fake Exception"))
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_AWAITING_NOTIFICATIONS, "State Awaiting Notifications should be reached after event subscribe error"
+
+ def test_stop_in_actions(self):
+ self.runner.start()
+
+ self.runner.stop()
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Stop state should not be reached"
+
+ self.pM._action_installed_cb('action1')
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_IDLE
+
+ def test_stop_in_events(self):
+ self.runner.start()
+ self.pM._action_installed_cb('action1')
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Setup events state should be reached after all actions installed"
+
+ self.runner.stop()
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Tutorial should not be stopped until all events have been confirmed"
+ self.pM.event_sub_cB('event1')
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_IDLE, "Tutorial should have been stopped right after the last event was confirmed"
+
+class TestInstallationStates(unittest.TestCase):
+ def setUp(self):
+ self.pM = MockProbeMgrMultiAddons()
+ self.tutorial = Tutorial("TutorialRunner")
+ self.state_name = self.tutorial.add_state()
+ self.tutorial.update_transition(Tutorial.INITIAL_TRANSITION_NAME,
+ None, self.state_name)
+ self.action1 = CountAction()
+ self.tutorial.add_action(self.state_name, self.action1)
+ self.action2 = CountAction()
+ self.tutorial.add_action(self.state_name, self.action2)
+
+ self.event = MockEvent()
+ self.tutorial.add_transition(self.state_name, (self.event, Tutorial.END))
+ self.event2 = MockEvent()
+ self.tutorial.add_transition(self.state_name, (self.event2, Tutorial.INIT))
+
+ self.runner = TutorialRunner(self.tutorial, self.pM)
+
+ def test_multiple_actions(self):
+ self.runner.start()
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Runner should be in Setup Actions state"
+
+ self.pM.run_install_cb(1, self.action2)
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Runner should still be in Setup Actions state after a single action confirmation callback"
+
+ self.pM.run_install_cb(0, self.action1)
+
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Runner should be in Setup Events state after all actions are installed"
+ self.pM.run_subscribe_cb(1)
+ assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Runner should still be in Setup Events state when not all event installations are confirmed"
-
class TutorialRunnerTest(unittest.TestCase):
"""
This class needs to test the TutorialRunner
@@ -73,7 +229,6 @@ class TutorialRunnerTest(unittest.TestCase):
def setUp(self):
self.pM = MockProbeMgr()
-
def tearDown(self):
self.pM = None
@@ -87,19 +242,20 @@ class TutorialRunnerTest(unittest.TestCase):
tutorial.add_transition(state_name, (event, Tutorial.END))
runner = TutorialRunner(tutorial, self.pM)
+
runner.start()
+ self.pM.event_sub_cB('event1')
- assert runner._state == state_name, "Current state is: %s"%runner._state
+ assert runner._state == state_name, "Current tutorial state is: %s"%runner._state
assert self.pM.action == None
assert self.pM.event == event
-
+
event.do_callback()
- assert runner._state == Tutorial.END, "Current state is: %s"%runner._state
+
+ assert runner._state == Tutorial.END, "Current tutorial state is: %s"%runner._state
assert self.pM.action == None, "Current action is %s"%str(self.pM.action)
assert self.pM.event == None, "Current event is %s"%str(self.pM.event)
-
-
# Limit cases
def testEmptyTutorial(self):
tutorial = Tutorial("TutorialRunner")
diff --git a/tests/linear_creatortests.py b/tests/linear_creatortests.py
deleted file mode 100644
index e3c30c1..0000000
--- a/tests/linear_creatortests.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# 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
-
-from sugar.tutorius.core import *
-from sugar.tutorius.actions import *
-from sugar.tutorius.filters import *
-from sugar.tutorius.linear_creator import *
-from sugar.tutorius.addons.triggereventfilter import *
-from actiontests import CountAction
-import unittest
-
-class CreatorTests(unittest.TestCase):
-
- def test_simple_usage(self):
- creator = LinearCreator()
- fsm_name = "SimpleUsageTest"
-
- creator.set_name(fsm_name)
-
- # Generate an FSM using the steps
- creator.action(CountAction())
- creator.action(CountAction())
-
- creator.event(TriggerEventFilter())
-
- creator.action(CountAction())
-
- creator.event(TriggerEventFilter())
-
- fsm = creator.generate_fsm()
-
- # Make sure everything worked!
- assert fsm.name == fsm_name, "Name was not set properly"
-
- init_state = fsm.get_state_by_name("INIT")
-
- assert len(init_state.get_action_list()) == 2, "Creator did not insert all the actions"
-
- assert init_state.get_event_filter_list()[0][1] == "State 1" , "expected next state to be 'State 1' but got %s" % init_state.get_event_filter_list()[0][1]
-
- state1 = fsm.get_state_by_name("State 1")
-
- assert len(state1.get_action_list()) == 1, "Creator did not insert all the actions"
-
- assert state1.get_event_filter_list()[0][1] == "State 2"
-
- # Make sure we have the final state and that it's empty
- state2 = fsm.get_state_by_name("State2")
-
- assert len(state2.get_action_list()) == 0, "Creator inserted extra actions on wrong state"
-
- assert len(state2.get_event_filter_list()) == 0, "Creator assigner events to the final state"
-
- creator.action(CountAction())
-
- fsm = creator.generate_fsm()
-
- state2 = fsm.get_state_by_name("State2")
-
- assert len(state2.get_action_list()) == 1, "Creator did not add the action"
-
- assert len(state2.get_event_filter_list()) == 0, "Creator assigner events to the final state"
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/tests/probetests.py b/tests/probetests.py
index 59072e5..37748d8 100644
--- a/tests/probetests.py
+++ b/tests/probetests.py
@@ -85,40 +85,43 @@ class MockProbeProxy(object):
@param activityName unique activity id. Must be a valid dbus bus name.
"""
self.MockAction = None
+ self.MockActionName = None
self.MockActionUpdate = None
self.MockEvent = None
self.MockCB = None
self.MockAlive = True
self.MockEventAddr = None
+ self.MockAddressCallback = None
def isAlive(self):
return self.MockAlive
- def install(self, action, block=False):
+ def install(self, action, action_installed_cb, error_cb):
self.MockAction = action
+ self.MockAddressCallback_install = action_installed_cb
+ self.MockInstallErrorCallback = error_cb
self.MockActionUpdate = None
return None
- def update(self, action, newaction, block=False):
- self.MockAction = action
+ def update(self, action_address, newaction, block=False):
+ self.MockActionAddress = action_address
self.MockActionUpdate = newaction
return None
- def uninstall(self, action, block=False):
+ def uninstall(self, action_address):
self.MockAction = None
self.MockActionUpdate = None
return None
- def subscribe(self, event, callback, block=True):
+ def subscribe(self, event, notif_cb, subscribe_cb, error_cb):
#Do like the current Probe
- if not block:
- raise RuntimeError("This function does not allow non-blocking mode yet")
-
- self.MockEvent= event
- self.MockCB = callback
+ self.MockEvent = event
+ self.MockCB = notif_cb
+ self.MockSubscribeCB = subscribe_cb
+ self.MockSubscriptionErrorCb = error_cb
return str(id(event))
- def unsubscribe(self, address, block=True):
+ def unsubscribe(self, address):
self.MockEventAddr = address
return None
@@ -343,29 +346,34 @@ class ProbeManagerTest(unittest.TestCase):
act2 = self.probeManager.get_registered_probes_list("act2")[0][1]
ad1 = MockAddon()
+ ad1_address = "Address1"
+ def callback(value):
+ pass
+ def error_cb():
+ pass
#ErrorCase: install, update, uninstall without currentActivity
#Action functions should do a warning if there is no activity
- self.assertRaises(RuntimeWarning, self.probeManager.install, ad1)
- self.assertRaises(RuntimeWarning, self.probeManager.update, ad1, ad1)
- self.assertRaises(RuntimeWarning, self.probeManager.uninstall, ad1)
+ self.assertRaises(RuntimeWarning, self.probeManager.install, ad1_address, ad1, callback)
+ self.assertRaises(RuntimeWarning, self.probeManager.update, ad1_address, ad1)
+ self.assertRaises(RuntimeWarning, self.probeManager.uninstall, ad1_address)
assert act1.MockAction is None, "Action should not be installed on inactive proxy"
assert act2.MockAction is None, "Action should not be installed on inactive proxy"
self.probeManager.currentActivity = "act1"
- self.probeManager.install(ad1)
+ self.probeManager.install(ad1, callback, error_cb)
assert act1.MockAction == ad1, "Action should have been installed"
assert act2.MockAction is None, "Action should not be installed on inactive proxy"
- self.probeManager.update(ad1, ad1)
+ self.probeManager.update(ad1_address, ad1)
assert act1.MockActionUpdate == ad1, "Action should have been updated"
assert act2.MockActionUpdate is None, "Should not update on inactive"
self.probeManager.currentActivity = "act2"
- self.probeManager.uninstall(ad1)
- assert act1.MockAction == ad1, "Action should still be installed"
+ self.probeManager.uninstall(ad1_address)
+ assert act1.MockActionAddress == ad1_address, "Action should still be installed"
self.probeManager.currentActivity = "act1"
- self.probeManager.uninstall(ad1)
+ self.probeManager.uninstall(ad1_address)
assert act1.MockAction is None, "Action should be uninstalled"
def test_events(self):
@@ -379,17 +387,19 @@ class ProbeManagerTest(unittest.TestCase):
ad2.i, ad2.s = (2, "test2")
cb1 = lambda *args: None
+ install_cb1 = lambda *args:None
+ error_cb1 = lambda *args:None
cb2 = lambda *args: None
#ErrorCase: unsubscribe and subscribe without current activity
#Event functions should do a warning if there is no activity
- self.assertRaises(RuntimeWarning, self.probeManager.subscribe, ad1, cb1)
+ self.assertRaises(RuntimeWarning, self.probeManager.subscribe, ad1, cb1, install_cb1, error_cb1)
self.assertRaises(RuntimeWarning, self.probeManager.unsubscribe, None)
assert act1.MockEvent is None, "No event should be on act1"
assert act2.MockEvent is None, "No event should be on act2"
self.probeManager.currentActivity = "act1"
- self.probeManager.subscribe(ad1, cb1)
+ self.probeManager.subscribe(ad1, cb1, install_cb1, error_cb1)
assert act1.MockEvent == ad1, "Event should have been installed"
assert act1.MockCB == cb1, "Callback should have been set"
assert act2.MockEvent is None, "No event should be on act2"
@@ -398,7 +408,6 @@ class ProbeManagerTest(unittest.TestCase):
assert act1.MockEventAddr == "SomeAddress", "Unsubscribe should have been called"
assert act2.MockEventAddr is None, "Unsubscribe should not have been called"
-
class ProbeProxyTest(unittest.TestCase):
def setUp(self):
dbus.SessionBus = MockSessionBus
@@ -422,46 +431,59 @@ class ProbeProxyTest(unittest.TestCase):
action.i, action.s = 5, "action"
action2 = MockAddon()
action2.i, action2.s = 10, "action2"
+ action2_address = "Addr2"
#Check if the installed action is the good one
address = "Addr1"
+
+ def action_installed_cb(value):
+ pass
+ def error_cb(value):
+ pass
+
#Set the return value of probe install
self.mockObj.MockRet["install"] = address
- self.probeProxy.install(action, block=True)
+ self.probeProxy.install(action, action_installed_cb, error_cb)
assert pickle.loads(self.mockObj.MockCall["install"]["args"][0]) == action, "1 argument, the action"
+ self.mockObj.MockCall["install"]["kwargs"]["reply_handler"](address)
#ErrorCase: Update should fail on noninstalled actions
- self.assertRaises(RuntimeWarning, self.probeProxy.update, action2, action2, block=True)
+ self.assertRaises(RuntimeWarning, self.probeProxy.update, action2_address, action2)
#Test the update
- self.probeProxy.update(action, action2, block=True)
+ self.probeProxy.update(address, action2)
args = self.mockObj.MockCall["update"]["args"]
assert args[0] == address, "arg 1 should be the action address"
assert pickle.loads(args[1]) == action2._props, "arg2 should be the new action properties"
#ErrorCase: Uninstall on not installed action (silent fail)
#Test the uninstall
- self.probeProxy.uninstall(action2, block=True)
+ self.probeProxy.uninstall(action2_address)
assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed"
- self.probeProxy.uninstall(action, block=True)
+ self.probeProxy.uninstall(address)
assert self.mockObj.MockCall["uninstall"]["args"][0] == address, "1 argument, the action address"
def test_events(self):
event = MockAddon()
event.i, event.s = 5, "event"
+ event_address = 'event1'
event2 = MockAddon()
event2.i, event2.s = 10, "event2"
+ event_address2 = 'event2'
def callback(event):
global message_box
message_box = event
+ subs_cb = lambda *args : None
+ error_cb = lambda *args : None
#Check if the installed event is the good one
address = "Addr1"
#Set the return value of probe subscribe
self.mockObj.MockRet["subscribe"] = address
- self.probeProxy.subscribe(event, callback, block=True)
+ self.probeProxy.subscribe(event, callback, subs_cb, error_cb)
+ self.probeProxy._ProbeProxy__update_event(event, callback, subs_cb, event_address)
assert pickle.loads(self.mockObj.MockCall["subscribe"]["args"][0]) == event, "1 argument, the event"
#Call the callback with the event
@@ -478,11 +500,11 @@ class ProbeProxyTest(unittest.TestCase):
#ErrorCase: unsubcribe for non subscribed event
#Test the unsubscribe
- self.probeProxy.unsubscribe("otheraddress", block=True)
+ self.probeProxy.unsubscribe(event_address2)
assert not "unsubscribe" in self.mockObj.MockCall, "Unsubscribe should not be called if event is not subscribeed"
- self.probeProxy.unsubscribe(address, block=True)
- assert self.mockObj.MockCall["unsubscribe"]["args"][0] == address, "1 argument, the event address"
+ self.probeProxy.unsubscribe(event_address)
+ assert self.mockObj.MockCall["unsubscribe"]["args"][0] == event_address, "1 argument, the event address"
#ErrorCase: eventOccured triggered by uninstalled event
#Test the callback with unregistered event
diff --git a/tests/propertiestests.py b/tests/propertiestests.py
index 2494ea6..cb8e884 100644
--- a/tests/propertiestests.py
+++ b/tests/propertiestests.py
@@ -540,6 +540,62 @@ class TFilePropertyTest(unittest.TestCase):
except FileConstraintError:
pass
+class TResourcePropertyTest(unittest.TestCase):
+ def test_valid_names(self):
+ class klass1(TPropContainer):
+ res = TResourceProperty()
+
+ name1 = "file_" + unicode(uuid.uuid1()) + ".png"
+ name2 = unicode(uuid.uuid1()) + "_" + unicode(uuid.uuid1()) + ".extension"
+ name3 = "/home/user/.sugar/_random/new_image1231_" + unicode(uuid.uuid1()).upper() + ".mp3"
+ name4 = "a_" + unicode(uuid.uuid1())
+ name5 = ""
+
+ obj1 = klass1()
+
+ obj1.res = name1
+ assert obj1.res == name1, "Could not assign the valid name correctly : %s" % name1
+
+ obj1.res = name2
+ assert obj1.res == name2, "Could not assign the valid name correctly : %s" % name2
+
+ obj1.res = name3
+ assert obj1.res == name3, "Could not assign the valid name correctly : %s" % name3
+
+ obj1.res = name4
+ assert obj1.res == name4, "Could not assign the valid name correctly : %s" % name4
+
+ obj1.res = name5
+ assert obj1.res == name5, "Could not assign the valid name correctly : %s" % name5
+
+ def test_invalid_names(self):
+ class klass1(TPropContainer):
+ res = TResourceProperty()
+
+ bad_name1 = ".jpg"
+ bad_name2 = "_.jpg"
+ bad_name3 = "_" + unicode(uuid.uuid1())
+
+ obj1 = klass1()
+
+ try:
+ obj1.res = bad_name1
+ assert False, "A invalid name was accepted : %s" % bad_name1
+ except ResourceConstraintError:
+ pass
+
+ try:
+ obj1.res = bad_name2
+ assert False, "A invalid name was accepted : %s" % bad_name2
+ except ResourceConstraintError:
+ pass
+
+ try:
+ obj1.res = bad_name3
+ assert False, "A invalid name was accepted : %s" % bad_name3
+ except ResourceConstraintError:
+ pass
+
class TAddonPropertyTest(unittest.TestCase):
def test_wrong_value(self):
class klass1(TPropContainer):
diff --git a/tests/translatortests.py b/tests/translatortests.py
new file mode 100644
index 0000000..3b5ca6f
--- /dev/null
+++ b/tests/translatortests.py
@@ -0,0 +1,131 @@
+# 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 unittest
+import os
+import uuid
+
+from sugar.tutorius.translator import *
+from sugar.tutorius.properties import *
+from sugar.tutorius.tutorial import *
+from sugar.tutorius.vault import Vault
+from sugar.tutorius import addon
+
+##############################################################################
+## Helper classes
+class ResourceAction(TPropContainer):
+ resource = TResourceProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+
+class NestedResource(TPropContainer):
+ nested = TAddonProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+ self.nested = ResourceAction()
+
+class ListResources(TPropContainer):
+ nested_list = TAddonListProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+ self.nested_list = [ResourceAction(), ResourceAction()]
+
+##
+##############################################################################
+
+class ResourceTranslatorTests(unittest.TestCase):
+ temp_path = "/tmp/"
+ file_name = "file.txt"
+
+ def setUp(self):
+ # Generate a tutorial ID
+ self.tutorial_id = unicode(uuid.uuid1())
+
+ # Create a dummy fsm
+ self.fsm = Tutorial("TestTutorial1")
+ # Add a few states
+ act1 = addon.create('BubbleMessage', message="Hi", position=[300, 450])
+ ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked")
+ act2 = addon.create('BubbleMessage', message="Second message", position=[250, 150], tail_pos=[1,2])
+ self.fsm.add_action("INIT", act1)
+ st2 = self.fsm.add_state((act2,))
+ self.fsm.add_transition("INIT",(ev1, st2))
+
+ # Create a dummy metadata dictionnary
+ self.test_metadata_dict = {}
+ self.test_metadata_dict['name'] = 'TestTutorial1'
+ self.test_metadata_dict['guid'] = unicode(self.tutorial_id)
+ self.test_metadata_dict['version'] = '1'
+ self.test_metadata_dict['description'] = 'This is a test tutorial 1'
+ self.test_metadata_dict['rating'] = '3.5'
+ self.test_metadata_dict['category'] = 'Test'
+ self.test_metadata_dict['publish_state'] = 'false'
+ activities_dict = {}
+ activities_dict['org.laptop.tutoriusactivity'] = '1'
+ activities_dict['org.laptop,writus'] = '1'
+ self.test_metadata_dict['activities'] = activities_dict
+
+ Vault.saveTutorial(self.fsm, self.test_metadata_dict)
+
+ try:
+ os.mkdir(self.temp_path)
+ except:
+ pass
+ abs_file_path = os.path.join(self.temp_path, self.file_name)
+ new_file = file(abs_file_path, "w")
+
+ # Add the resource in the Vault
+ self.res_name = Vault.add_resource(self.tutorial_id, abs_file_path)
+
+ # Use a dummy prob manager - we shouldn't be using it
+ self.prob_man = object()
+
+ self.translator = ResourceTranslator(self.prob_man, self.tutorial_id)
+
+ def tearDown(self):
+ Vault.deleteTutorial(self.tutorial_id)
+
+ os.unlink(os.path.join(self.temp_path, self.file_name))
+
+ def test_translate(self):
+ # Create an action with a resource property
+ res_action = ResourceAction()
+ res_action.resource = self.res_name
+
+ self.translator.translate(res_action)
+
+ assert getattr(res_action, "resource").type == "file", "Resource was not converted to file"
+
+ assert res_action.resource.default == Vault.get_resource_path(self.tutorial_id, self.res_name), "Transformed resource path is not the same as the one given by the vault"
+
+ def test_recursive_translate(self):
+ nested_action = NestedResource()
+
+ self.translator.translate(nested_action)
+
+ assert getattr(getattr(nested_action, "nested"), "resource").type == "file", "Nested resource was not converted properly"
+
+ def test_list_translate(self):
+ list_action = ListResources()
+
+ self.translator.translate(list_action)
+
+ for container in list_action.nested_list:
+ assert getattr(container, "resource").type == "file", "Element of list was not converted properly"
+
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
index 1521eab..7021f80 100644
--- a/tutorius/TProbe.py
+++ b/tutorius/TProbe.py
@@ -1,10 +1,25 @@
+# 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 1 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 logging
LOGGER = logging.getLogger("sugar.tutorius.TProbe")
import os
import gobject
-import dbus
import dbus.service
import cPickle as pickle
@@ -12,7 +27,6 @@ import cPickle as pickle
from . import addon
from . import properties
from .services import ObjectStore
-from .properties import TPropContainer
from .dbustools import remote_call, save_args
import copy
@@ -78,8 +92,6 @@ class TProbe(dbus.service.Object):
LOGGER.debug("TProbe :: registering '%s' with unique_id '%s'", self._activity_name, activity.get_id())
self._service_proxy.register_probe(self._activity_name, self._unique_id)
-
-
def start(self):
"""
@@ -184,32 +196,12 @@ class TProbe(dbus.service.Object):
in_signature='s', out_signature='s')
def create_event(self, addon_name):
# avoid recursive imports
- from .creator import WidgetSelector, SignalInputDialog, TextInputDialog
-
event = addon.create(addon_name)
addonname = type(event).__name__
meta = addon.get_addon_meta(addonname)
for propname in meta['mandatory_props']:
prop = getattr(type(event), propname)
- if isinstance(prop, properties.TUAMProperty):
- selector = WidgetSelector(self._activity)
- setattr(event, propname, selector.select())
- elif isinstance(prop, properties.TEventType):
- try:
- dlg = SignalInputDialog(self._activity,
- text="Mandatory property",
- field=propname,
- addr=event.object_id)
- setattr(event, propname, dlg.pop())
- except AttributeError:
- pass
- elif isinstance(prop, properties.TStringProperty):
- dlg = TextInputDialog(self._activity,
- text="Mandatory property",
- field=propname)
- setattr(event, propname, dlg.pop())
- else:
- raise NotImplementedError()
+ prop.widget_class.run_dialog(self._activity, event, propname)
return pickle.dumps(event)
@@ -322,7 +314,6 @@ class ProbeProxy:
self._subscribedEvents = {}
self._registeredCallbacks = {}
-
self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface")
def _handle_signal(self, pickled_event):
@@ -344,58 +335,57 @@ class ProbeProxy:
except:
return False
- def __update_action(self, action, address):
+ def __update_action(self, action, callback, address):
LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address))
- self._actions[action] = str(address)
+ self._actions[address] = action
+ callback(address)
- def __clear_action(self, action):
- self._actions.pop(action, None)
+ def __clear_action(self, address):
+ self._actions.pop(address, None)
- def install(self, action, block=False, is_editing=False):
+ def install(self, action, action_installed_cb, error_cb, is_editing=False):
"""
Install an action on the TProbe's activity
@param action Action to install
- @param block Force a synchroneous dbus call if True
+ @param action_installed_cb The callback function to call once the action is installed
+ @param error_cb The callback function to call when an error happens
@param is_editing whether this action comes from the editor
@return None
"""
- return remote_call(self._probe.install,
- (pickle.dumps(action), is_editing),
- save_args(self.__update_action, action),
- block=block)
+ self._probe.install(pickle.dumps(action), is_editing,
+ reply_handler=save_args(self.__update_action, action, action_installed_cb),
+ error_handler=save_args(error_cb, action))
- def update(self, action, newaction, block=False, is_editing=False):
+ def update(self, action_address, newaction, is_editing=False):
"""
Update an already installed action's properties and run it again
- @param action Action to update
+ @param action_address The address of the action to update. This is
+ provided by the install callback method.
@param newaction Action to update it with
@param block Force a synchroneous dbus call if True
@param is_editing whether this action comes from the editor
@return None
"""
#TODO review how to make this work well
- if not action in self._actions.keys():
+ if not action_address in self._actions.keys():
raise RuntimeWarning("Action not installed")
#TODO Check error handling
- return remote_call(self._probe.update,
- (self._actions[action],
- pickle.dumps(newaction._props),
- is_editing),
- block=block)
+ return self._probe.update(action_address, pickle.dumps(newaction._props), is_editing,
+ reply_handler=ignore,
+ error_handler=logError)
- def uninstall(self, action, block=False, is_editing=False):
+ def uninstall(self, action_address, is_editing):
"""
Uninstall an installed action
- @param action Action to uninstall
- @param block Force a synchroneous dbus call if True
+ @param action_address The address of the action to uninstall. This address was given
+ on action installation
@param is_editing whether this action comes from the editor
"""
- if action in self._actions.keys():
- remote_call(self._probe.uninstall,
- (self._actions.pop(action), is_editing),
- block=block)
+ if action_address in self._actions:
+ self._actions.pop(action_address, None)
+ self._probe.uninstall(action_address, is_editing, reply_handler=ignore, error_handler=logError)
- def __update_event(self, event, callback, address):
+ def __update_event(self, event, callback, event_subscribed_cb, address):
LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address))
# Since multiple callbacks could be associated to the same
# event signature, we will store multiple callbacks
@@ -411,7 +401,7 @@ class ProbeProxy:
# 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?
+ # Oops, how come we have two similar addresses?
# send the bad news!
raise Exception("Probe subscribe exception, the following address already exists: " + str(address))
@@ -424,6 +414,7 @@ class ProbeProxy:
# our dictionary (python pass arguments by reference)
self._subscribedEvents[address] = copy.copy(event)
+ event_subscribed_cb(address)
return address
def __clear_event(self, address):
@@ -453,24 +444,26 @@ class ProbeProxy:
"""
return pickle.loads(str(self._probe.create_event(addon_name)))
- def subscribe(self, event, callback, block=True):
+ def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb):
+
"""
Register an event listener
@param event Event to listen for
- @param callback callable that will be called when the event occurs
- @param block Force a synchroneous dbus call if True (Not allowed yet)
+ @param notification_cb callable that will be called when the event occurs
+ @param event_installed_cb callable that will be called once the event is subscribed to
+ @param error_cb callable that will be called if the subscription fails
@return address identifier used for unsubscribing
"""
LOGGER.debug("ProbeProxy :: Registering event %s", str(hash(event)))
- if not block:
- raise RuntimeError("This function does not allow non-blocking mode yet")
+ #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)
+ self._probe.subscribe(pickle.dumps(event),
+ reply_handler=save_args(self.__update_event, event, notification_cb, event_subscribed_cb),
+ error_handler=save_args(error_cb, event))
def unsubscribe(self, address, block=True):
"""
@@ -481,9 +474,10 @@ class ProbeProxy:
"""
LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address))
if address in self._subscribedEvents.keys():
- remote_call(self._probe.unsubscribe, (address,),
- return_cb=save_args(self.__clear_event, address),
- block=block)
+ self.__clear_event(address)
+ self._probe.unsubscribe(address,
+ reply_handler=save_args(self.__clear_event, address),
+ error_handler=logError)
else:
LOGGER.debug("ProbeProxy :: unsubscribe address %s failed : not registered", address)
@@ -493,10 +487,10 @@ class ProbeProxy:
subscribed events should be removed.
"""
for action_addr in self._actions.keys():
- self.uninstall(action_addr, block)
+ self.uninstall(action_addr)
for address in self._subscribedEvents.keys():
- self.unsubscribe(address, block)
+ self.unsubscribe(address)
class ProbeManager(object):
@@ -536,43 +530,48 @@ class ProbeManager(object):
currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
- def install(self, action, block=False, is_editing=False):
+ def install(self, action, action_installed_cb, error_cb, is_editing=False):
"""
Install an action on the current activity
@param action Action to install
+ @param action_installed_cb The callback to call once the action is installed
+ @param error_cb The callback that will be called if there is an error during installation
@param block Force a synchroneous dbus call if True
@param is_editing whether this action comes from the editor
@return None
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).install(action, block, is_editing)
+ return self._first_proxy(self.currentActivity).install(
+ action=action,
+ is_editing=is_editing,
+ action_installed_cb=action_installed_cb,
+ error_cb=error_cb)
else:
raise RuntimeWarning("No activity attached")
- def update(self, action, newaction, block=False, is_editing=False):
+ def update(self, action_address, newaction, is_editing=False):
"""
Update an already installed action's properties and run it again
- @param action Action to update
+ @param action_address Action to update
@param newaction Action to update it with
@param block Force a synchroneous dbus call if True
@param is_editing whether this action comes from the editor
@return None
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).update(action, newaction, block, is_editing)
+ return self._first_proxy(self.currentActivity).update(action_address, newaction, is_editing)
else:
raise RuntimeWarning("No activity attached")
- def uninstall(self, action, block=False, is_editing=False):
+ def uninstall(self, action_address, is_editing=False):
"""
Uninstall an installed action
- @param action Action to uninstall
+ @param action_address Action to uninstall
@param block Force a synchroneous dbus call if True
@param is_editing whether this action comes from the editor
"""
if self.currentActivity:
- # return self._probes[self.currentActivity].uninstall(action, block, is_editing)
- return self._first_proxy(self.currentActivity).uninstall(action, block, is_editing)
+ return self._first_proxy(self.currentActivity).uninstall(action_address, is_editing)
else:
raise RuntimeWarning("No activity attached")
@@ -585,19 +584,24 @@ class ProbeManager(object):
@returns: an eventfilter instance
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).uninstall(action, block)
+ return self._first_proxy(self.currentActivity).create_event(addon_name)
else:
raise RuntimeWarning("No activity attached")
- def subscribe(self, event, callback):
+ def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb):
"""
Register an event listener
@param event Event to listen for
- @param callback callable that will be called when the event occurs
+ @param notification_cb callable that will be called when the event occurs
+ @param subscribe_cb callable that will be called once the action has been
+ installed
+ @param error_cb callable that will be called if an error happens during
+ installation
@return address identifier used for unsubscribing
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).subscribe(event, callback)
+ return self._first_proxy(self.currentActivity).subscribe(event, notification_cb,\
+ event_subscribed_cb, error_cb)
else:
raise RuntimeWarning("No activity attached")
diff --git a/tutorius/addon.py b/tutorius/addon.py
index 21ebffa..6e3d8b9 100644
--- a/tutorius/addon.py
+++ b/tutorius/addon.py
@@ -66,10 +66,10 @@ def create(name, *args, **kwargs):
try:
return comp_metadata['class'](*args, **kwargs)
except:
- logging.error("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs)))
+ logging.debug("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs)))
return None
except KeyError:
- logging.error("Addon not found for class '%s'", name)
+ logging.debug("Addon not found for class '%s'", name)
return None
def list_addons():
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 519bce8..cd71167 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -24,6 +24,8 @@ for some properties.
# For the File Constraint
import os
+# For the Resource Constraint
+import re
class ConstraintException(Exception):
"""
@@ -214,3 +216,40 @@ class FileConstraint(Constraint):
raise FileConstraintError("Non-existing file : %s"%value)
return
+class ResourceConstraintError(ConstraintException):
+ pass
+
+class ResourceConstraint(Constraint):
+ """
+ Ensures that the value is looking like a resource name, like
+ <filename>_<GUID>[.<extension>]. We are not validating that this is a
+ valid resource for the reason that the property does not have any notion
+ of tutorial guid.
+
+ TODO : Find a way to properly validate resources by looking them up in the
+ Vault.
+ """
+
+ # Regular expression to parse a resource-like name
+ resource_regexp_text = "(.+)_([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})(\..*)?$"
+ resource_regexp = re.compile(resource_regexp_text)
+
+ def validate(self, value):
+ # TODO : Validate that we will not use an empty resource or if we can
+ # have transitory resource names
+ if value is None:
+ raise ResourceConstraintError("Resource not allowed to have a null value!")
+
+ # Special case : We allow the empty resource name for now
+ if value == "":
+ return value
+
+ # Attempt to see if the value has a resource name inside it
+ match = self.resource_regexp.search(value)
+
+ # If there was no match on the reg exp
+ if not match:
+ raise ResourceConstraintError("Resource name does not seem to be valid : %s" % value)
+
+ # If the name matched, then the value is _PROBABLY_ good
+ return value
diff --git a/tutorius/creator.py b/tutorius/creator.py
index e8182b0..54e2912 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -37,6 +37,8 @@ from . import viewer
from .propwidgets import TextInputDialog
from . import TProbe
+from functools import partial
+
from dbus import SessionBus
from dbus.service import method, Object, BusName
@@ -75,6 +77,7 @@ class Creator(Object):
self.is_authoring = False
Creator._instance = self
self._probe_mgr = TProbe.ProbeManager.default_instance
+ self._installed_actions = list()
def start_authoring(self, tutorial=None):
"""
@@ -177,8 +180,8 @@ class Creator(Object):
.get(action, None)
if not action_obj:
return False
- #action_obj.exit_editmode()
- self._probe_mgr.uninstall(action_obj, is_editing=True)
+
+ self._probe_mgr.uninstall(action_obj.address)
self._tutorial.delete_action(action)
self._overview.win.queue_draw()
return True
@@ -225,47 +228,43 @@ class Creator(Object):
or state_name == self._tutorial.END:
return
- for action in self._tutorial.get_action_dict(self._state).values():
- #action.exit_editmode()
- self._probe_mgr.uninstall(action, is_editing=True)
+ for action in self._installed_actions:
+ self._probe_mgr.uninstall(action.address,
+ is_editing=True)
+ self._installed_actions = []
self._state = state_name
state_actions = self._tutorial.get_action_dict(self._state).values()
+
for action in state_actions:
- action.enter_editmode()
- action._drag._eventbox.connect_after(
- "button-release-event", self._action_refresh_cb, action)
+ return_cb = partial(self._action_installed_cb, action)
+ self._probe_mgr.install(action,
+ action_installed_cb=return_cb,
+ error_cb=self._dbus_exception,
+ is_editing=True)
if state_actions:
+ # I'm really lazy right now and to keep things simple I simply
+ # always select the first action when
+ # we change state. we should really select the clicked block
+ # in the overview instead. FIXME
self._propedit.action = state_actions[0]
else:
self._propedit.action = None
self._overview.win.queue_draw()
-
- def _evfilt_cb(self, menuitem, event):
- """
- This will get called once the user has selected a menu item from the
- event filter popup menu. This should add the correct event filter
- to the FSM and increment states.
- """
- # undo actions so they don't persist through step editing
- for action in self._state.get_action_list():
- self._probe_mgr.uninstall(action, is_editing=True)
- #action.exit_editmode()
- self._propedit.action = None
- #self._activity.queue_draw()
-
def _add_action_cb(self, widget, path):
"""Callback for the action creation toolbar tool"""
action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME]
action = addon.create(action_type)
- self._probe_mgr.install(action, is_editing=True)
+ return_cb = partial(self._action_installed_cb, action)
+ self._probe_mgr.install(action,
+ action_installed_cb=return_cb,
+ error_cb=self._dbus_exception,
+ is_editing=True)
self._tutorial.add_action(self._state, action)
- # FIXME: replace following with event catching
- #action._drag._eventbox.connect_after(
- # "button-release-event", self._action_refresh_cb, action)
+ self._propedit.action = action
self._overview.win.queue_draw()
def _add_event_cb(self, widget, path):
@@ -290,12 +289,7 @@ class Creator(Object):
"""
event_type = self._propedit.events_list[path][ToolBox.ICON_NAME]
- event = addon.create(event_type)
- addonname = type(event).__name__
- meta = addon.get_addon_meta(addonname)
- for propname in meta['mandatory_props']:
- prop = getattr(type(event), propname)
- prop.widget_class.run_dialog(None, event, propname)
+ event = self._probe_mgr.create_event(event_type)
event_filters = self._tutorial.get_transition_dict(self._state)
@@ -328,14 +322,13 @@ class Creator(Object):
Callback for refreshing properties values and notifying the
property dialog of the new values.
"""
- self._probe_mgr.uninstall(action, is_editing=True)
- #action.exit_editmode()
- self._probe_mgr.install(action, is_editing=True)
- #action.enter_editmode()
- #self._activity.queue_draw()
- # TODO: replace following with event catching
- #action._drag._eventbox.connect_after(
- # "button-release-event", self._action_refresh_cb, action)
+ self._probe_mgr.uninstall(action.address,
+ is_editing=True)
+ return_cb = partial(self._action_installed_cb, action)
+ self._probe_mgr.install(action,
+ action_installed_cb=return_cb,
+ error_cb=self._dbus_exception,
+ is_editing=True)
self._propedit.action = action
self._overview.win.queue_draw()
@@ -348,12 +341,12 @@ class Creator(Object):
"""
# undo actions so they don't persist through step editing
for action in self._tutorial.get_action_dict(self._state).values():
- #action.exit_editmode()
- self._probe_mgr.uninstall(action, is_editing=True)
+ self._probe_mgr.uninstall(action.address,
+ is_editing=True)
if kwargs.get('force', False):
dialog = gtk.MessageDialog(
- parent=None,
+ parent=self._overview.win,
flags=gtk.DIALOG_MODAL,
type=gtk.MESSAGE_QUESTION,
buttons=gtk.BUTTONS_YES_NO,
@@ -398,6 +391,23 @@ class Creator(Object):
assert False, "REMOVE THIS CALL!!!"
launch = staticmethod(launch)
+ def _action_installed_cb(self, action, address):
+ """
+ This is a callback intented to be use to receive actions addresses
+ after they are installed.
+ @param address: the address of the newly installed action
+ """
+ action.address = address
+ self._installed_actions.append(action)
+
+ def _dbus_exception(self, exception):
+ """
+ This is a callback intented to be use to receive exceptions on remote
+ DBUS calls.
+ @param exception: the exception thrown by the remote process
+ """
+ pass
+
class ToolBox(object):
"""
diff --git a/tutorius/dbustools.py b/tutorius/dbustools.py
index 02acd3d..0973164 100644
--- a/tutorius/dbustools.py
+++ b/tutorius/dbustools.py
@@ -1,3 +1,19 @@
+# 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 1 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 logging
import gobject
diff --git a/tutorius/engine.py b/tutorius/engine.py
index c945e49..198fa11 100644
--- a/tutorius/engine.py
+++ b/tutorius/engine.py
@@ -1,4 +1,21 @@
+# 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 1 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 logging
+from heapq import heappush, heappop
import dbus.mainloop.glib
from jarabe.model import shell
from sugar.bundle.activitybundle import ActivityBundle
@@ -7,7 +24,21 @@ from .vault import Vault
from .TProbe import ProbeManager
from .dbustools import save_args
from .tutorial import Tutorial, AutomaticTransitionEvent
+from .translator import ResourceTranslator
+
+# Priority values for queuable messages
+STOP_MSG_PRIORITY = 5
+EVENT_NOTIFICATION_MSG_PRIORITY = 10
+# List of runner states
+RUNNER_STATE_IDLE = "idle"
+RUNNER_STATE_SETUP_ACTIONS = "setup_actions"
+RUNNER_STATE_SETUP_EVENTS = "setup_events"
+RUNNER_STATE_AWAITING_NOTIFICATIONS = "awaiting_notification"
+RUNNER_STATE_UNINSTALLING_ACTIONS = "uninstalling_actions"
+RUNNER_STATE_UNSUBSCRIBING_EVENTS = "unsubscribing_events"
+
+LOGGER = logging.getLogger("sugar.tutorius.engine")
class TutorialRunner(object):
"""
@@ -21,12 +52,27 @@ class TutorialRunner(object):
self._tutorial = tutorial
self._pM = probeManager
+ # The tutorial runner's state. For more details, see :
+ # https://docs.google.com/Doc?docid=0AVT_nzmWT2B2ZGN3dDd2MzRfNTBka3J4bW1kaA&hl=en
+ self._runner_state = RUNNER_STATE_IDLE
+
+ # The message queue is a heap, so only heap operations should be done
+ # on it like heappush, heappop, etc...
+ # The stocked messages are actually a list of parameters that should be
+ # passed to the appropriate function. E.g. When raising an event notification,
+ # it saves the (next_state, event) in the message.
+ self._message_queue = []
+
#State
self._state = None
- self._sEvents = set() #Subscribed Events
#Cached objects
- self._actions = {}
+ self._installed_actions = {}
+ self._installation_errors = {}
+
+ # Subscribed Events
+ self._subscribed_events = {}
+ self._subscription_errors = {}
#Temp FIX until event/actions have an activity id
self._activity_id = None
@@ -34,86 +80,216 @@ class TutorialRunner(object):
#Temp FIX until event, actions have an activity id
def setCurrentActivity(self):
self._pM.currentActivity = self._activity_id
-
+
+ ###########################################################################
+ # Incoming messages
def start(self):
self.setCurrentActivity() #Temp Hack until activity in events/actions
self.enterState(self._tutorial.INIT)
def stop(self):
+ if self._runner_state == RUNNER_STATE_SETUP_ACTIONS or \
+ self._runner_state == RUNNER_STATE_SETUP_EVENTS:
+ heappush(self._message_queue, (STOP_MSG_PRIORITY, None))
+ elif self._runner_state != RUNNER_STATE_IDLE:
+ self._execute_stop()
+
+ def action_installed(self, action_name, address):
+ LOGGER.debug("TutorialRunner :: Action %s received address %s"%(action_name, address))
+ self._installed_actions[action_name] = address
+ # Verify if we just completed the installation of the actions for this state
+ self._verify_action_install_state()
+
+ def install_error(self, action_name, action, exception):
+ # TODO : Fix this as it doesn't warn the user about the problem or anything
+ LOGGER.debug("TutorialRunner :: Action could not be installed %s, exception was : %s"%(str(action), str(exception)))
+ self._installation_errors[action_name] = exception
+ self._verify_action_install_state()
+
+ def event_subscribed(self, event_name, event_address):
+ LOGGER.debug("TutorialRunner :: Event %s was subscribed to, located at address %s"%(event_name, event_address))
+ self._subscribed_events[event_name] = event_address
+
+ # Verify if we just completed the subscription of all the events for this state
+ self._verify_event_install_state()
+
+ def subscribe_error(self, event_name, exception):
+ # TODO : Do correct error handling here
+ LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : %s"%(event_name, str(exception)))
+ self._subscription_errors[event_name] = exception
+
+ # Verify if we just completed the subscription of all the events for this state
+ self._verify_event_install_state()
+
+ def all_actions_installed(self):
+ self._runner_state = RUNNER_STATE_SETUP_EVENTS
+ # Process the messages that might have been stored
+ self._process_pending_messages()
+
+ # If we processed a message that changed the runner state, we need to stop
+ # processing
+ if self._runner_state != RUNNER_STATE_SETUP_EVENTS:
+ return
+
+ # Start subscribing to events
+ transitions = self._tutorial.get_transition_dict(self._state)
+
+ # If there are no transitions, raise the All Events Subscribed message
+ if len(transitions) == 0:
+ self.all_events_subscribed()
+ return
+
+ # Send all the event registration
+ for (event_name, (event, next_state)) in transitions.items():
+ self._pM.subscribe(event,
+ save_args(self._handleEvent, next_state),
+ save_args(self.event_subscribed, event_name),
+ save_args(self.subscribe_error, event_name))
+
+ def all_events_subscribed(self):
+ self._runner_state = RUNNER_STATE_AWAITING_NOTIFICATIONS
+ self._process_pending_messages()
+
+ ###########################################################################
+ # Helper functions
+ def _execute_stop(self):
self.setCurrentActivity() #Temp Hack until activity in events/actions
- self.enterState(self._tutorial.END)
self._teardownState()
self._state = None
+ self._runner_state = RUNNER_STATE_IDLE
def _handleEvent(self, next_state, event):
- #FIXME sanity check, log event that was not installed and ignore
- self.enterState(next_state)
+ # Look if we are actually receiving notifications
+ if self._runner_state == RUNNER_STATE_AWAITING_NOTIFICATIONS:
+ LOGGER.debug("TutorialRunner :: Received event notification in AWAITING_NOTIFICATIONS for %s"%str(event))
+ transitions = self._tutorial.get_transition_dict(self._state)
+ for (this_event, this_next_state_name) in transitions.values():
+ if event == this_event and next_state == this_next_state_name:
+ self.enterState(next_state)
+ break
+ elif self._runner_state == RUNNER_STATE_SETUP_EVENTS:
+ LOGGER.debug("TutorialRunner :: Queuing event notification to go to state %s"%next_state)
+ # Push the message on the queue
+ heappush(self._message_queue, (EVENT_NOTIFICATION_MSG_PRIORITY, (next_state, event)))
+ # Ignore the message for all other states
def _teardownState(self):
if self._state is None:
#No state, no teardown
return
+ self._remove_installed_actions()
+ self._remove_subscribed_events()
+ def _remove_installed_actions(self):
#Clear the current actions
- for action in self._actions.values():
- self._pM.uninstall(action)
- self._actions = {}
+ for (action_name, action_address) in self._installed_actions.items():
+ LOGGER.debug("TutorialRunner :: Uninstalling action %s with address %s"%(action_name, action_address))
+ self._pM.uninstall(action_address)
+ self._installed_actions.clear()
+ self._installation_errors.clear()
+ def _remove_subscribed_events(self):
#Clear the EventFilters
- for event in self._sEvents:
- self._pM.unsubscribe(event)
- self._sEvents.clear()
-
- def _setupState(self):
- if self._state is None:
- raise RuntimeError("Attempting to setupState without a state")
-
- # Handle the automatic event
- state_name = self._state
-
- self._actions = self._tutorial.get_action_dict(self._state)
+ for (event_name, event_address) in self._subscribed_events.items():
+ self._pM.unsubscribe(event_address)
+ self._subscribed_events.clear()
+ self._subscription_errors.clear()
+
+ def _verify_action_install_state(self):
+ actions = self._tutorial.get_action_dict(self._state)
+
+ # Do the check to see if we have finished installing all the actions by either having
+ # received a address for it or an error message
+ install_complete = True
+ for (this_action_name, this_action) in actions.items():
+ if not this_action_name in self._installed_actions.keys() and \
+ not this_action_name in self._installation_errors.keys():
+ # There's at least one uninstalled action, so we still wait
+ install_complete = False
+ break
+
+ if install_complete:
+ LOGGER.debug("TutorialRunner :: All actions installed!")
+ # Raise the All Actions Installed event for the TutorialRunner state
+ self.all_actions_installed()
+
+ def _verify_event_install_state(self):
transitions = self._tutorial.get_transition_dict(self._state)
- for (event, next_state) in transitions.values():
- if isinstance(event, AutomaticTransitionEvent):
- state_name = next_state
+ # Check to see if we completed all the event subscriptions
+ subscribe_complete = True
+ for (this_event_name, (this_event, next_state)) in transitions.items():
+ if not this_event_name in self._subscribed_events.keys() and \
+ not this_event_name in self._subscription_errors.keys():
+ subscribe_complete = False
break
+
+ if subscribe_complete:
+ LOGGER.debug("TutorialRunner : Subscribed to all events!")
+ self.all_events_subscribed()
+
+ def _process_pending_messages(self):
+ while len(self._message_queue) != 0:
+ (priority, message) = heappop(self._message_queue)
+
+ if priority == STOP_MSG_PRIORITY:
+ LOGGER.debug("TutorialRunner :: Stop message taken from message queue")
+ # We can safely ignore the rest of the events
+ self._message_queue = []
+ self._execute_stop()
+ elif priority == EVENT_NOTIFICATION_MSG_PRIORITY:
+ LOGGER.debug("TutorialRunner :: Handling stored event notification for next_state %s"%message[0])
+ self._handle_event(*message)
- self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state)))
+ def _setupState(self):
+ if self._state is None:
+ raise RuntimeError("Attempting to setupState without a state")
- for action in self._actions.values():
- self._pM.install(action)
+ actions = self._tutorial.get_action_dict(self._state)
+
+ if len(actions) == 0:
+ self.all_actions_installed()
+ return
- return state_name
+ for (action_name, action) in actions.items():
+ LOGGER.debug("TutorialRunner :: Installed action %s"%(action_name))
+ self._pM.install(action,
+ save_args(self.action_installed, action_name),
+ save_args(self.install_error, action_name))
def enterState(self, state_name):
"""
- Starting from the state_name, the runner execute states until
- no automatic transition are found and will wait for an external
- event to occur.
+ Starting from the state_name, the runner execute states from the
+ tutorial until no automatic transitions are found and will wait
+ for an external event to occur.
- When entering the state, actions and events from the previous
+ When entering the sate, actions and events from the previous
state are respectively uninstalled and unsubscribed and actions
and events from the state_name will be installed and subscribed.
@param state_name The name of the state to enter in
"""
self.setCurrentActivity() #Temp Hack until activity in events/actions
+ # Set the runner state to actions setup
+ self._runner_state = RUNNER_STATE_SETUP_ACTIONS
- # Recursive base case
- if state_name == self._state:
- #Nothing to do
- return
+ real_next_state = None
+ skip_to_state = state_name
+
+ # As long as we do have automatic transitions, skip them to go to the
+ # next state
+ while skip_to_state != real_next_state:
+ real_next_state = skip_to_state
+ transitions = self._tutorial.get_transition_dict(skip_to_state)
+ for (event, next_state) in transitions.values():
+ if isinstance(event, AutomaticTransitionEvent):
+ skip_to_state = next_state
+ break
self._teardownState()
- self._state = state_name
-
- # Recursively call the enterState in case there was an automatic
- # transition in the state definition
- self.enterState(self._setupState())
-
-
+ self._state = real_next_state
+ self._setupState()
class Engine:
"""
@@ -137,7 +313,8 @@ class Engine:
if self._tutorial:
self.stop()
- self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), self._probeManager)
+ translator_decorator = ResourceTranslator(self._probeManager, tutorialID)
+ self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), translator_decorator)
#Get the active activity from the shell
activity = self._shell.get_active_activity()
diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py
deleted file mode 100644
index f664c49..0000000
--- a/tutorius/linear_creator.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# 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
-
-from copy import deepcopy
-
-from .core import *
-from .actions import *
-from .filters import *
-
-class LinearCreator(object):
- """
- This class is used to create a FSM from a linear sequence of orders. The
- orders themselves are meant to be either an action or a transition.
- """
-
- def __init__(self):
- self.fsm = FiniteStateMachine("Sample Tutorial")
- self.current_actions = []
- self.nb_state = 0
- self.state_name = "INIT"
-
- def set_name(self, name):
- """
- Sets the name of the generated FSM.
- """
- self.fsm.name = name
-
- def action(self, action):
- """
- Adds an action to execute in the current state.
- """
- self.current_actions.append(action)
-
- def event(self, event_filter):
- """
- Adds a transition to another state. When executing this, all the actions
- previously called will be bundled in a single state, with the exit
- condition of this state being the transition just added.
-
- Whatever the name of the next state you inserted in the event, it will
- be replaced to point to the next event in the line.
- """
- if len(self.current_actions) != 0:
- # 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)
- state = State(self.state_name, action_list=self.current_actions,
- event_filter_list=[(event_filter, next_state_name),])
- self.state_name = next_state_name
-
- self.nb_state += 1
- self.fsm.add_state(state)
-
- # Clear the actions from the list
- self.current_actions = []
-
- def generate_fsm(self):
- """
- Returns a finite state machine corresponding to the sequence of calls
- that were made from this point on.
- """
- # Copy the whole FSM that was generated yet
- new_fsm = deepcopy(self.fsm)
-
- # Generate the final state
- state = None
- if len(self.current_actions) != 0:
- state = State("State" + str(self.nb_state), action_list=self.current_actions)
- # Don't increment the nb_state here - we would break the linearity
- # because we might generate more stuff with this creator later.
- # Since we rely on linearity for continuity when generating the
- # next state's name on an event filter, we cannot increment here.
- else:
- state = State("State" + str(self.nb_state))
-
- # Insert the state in the copy of the FSM
- new_fsm.add_state(state)
-
- return new_fsm
-
diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py
index b967739..9e4adbf 100644
--- a/tutorius/overlayer.py
+++ b/tutorius/overlayer.py
@@ -149,8 +149,8 @@ class Overlayer(gtk.Layout):
# Since widget is laid out in a Layout box, the Layout will honor the
# requested size. Using size_allocate could make a nasty nested loop in
# some cases.
- self._overlayed.set_size_request(allocation.width, allocation.height)
-
+ if self._overlayed:
+ self._overlayed.set_size_request(allocation.width, allocation.height)
class TextBubble(gtk.Widget):
"""
@@ -319,6 +319,198 @@ class TextBubble(gtk.Widget):
gobject.type_register(TextBubble)
+class TextBubbleWImg(gtk.Widget):
+ """
+ A CanvasDrawableWidget drawing a round textbox and a tail pointing
+ to a specified widget.
+ """
+ def __init__(self, text, speaker=None, tailpos=(0,0), imagepath=None):
+ """
+ Creates a new cairo rendered text bubble.
+
+ @param text the text to render in the bubble
+ @param speaker the widget to compute the tail position from
+ @param tailpos (optional) position relative to the bubble to use as
+ the tail position, if no speaker
+ """
+ gtk.Widget.__init__(self)
+
+ # FIXME: ensure previous call does not interfere with widget stacking,
+ # as using a gtk.Layout and stacking widgets may reveal a screwed up
+ # order with the cairo widget on top.
+ self.__label = None
+
+ self.label = text
+ self.speaker = speaker
+ self.tailpos = tailpos
+ self.line_width = 5
+ self.padding = 20
+
+ # image painting
+ self.filename = imagepath
+ self.pixbuf = gtk.gdk.pixbuf_new_from_file(self.filename)
+ self.imgsize = (self.pixbuf.get_width(), self.pixbuf.get_height())
+
+ self._no_expose = False
+ self.__exposer = None
+
+ def draw_with_context(self, context):
+ """
+ Draw using the passed cairo context instead of creating a new cairo
+ context. This eases blending between multiple cairo-rendered
+ widgets.
+ """
+ context.translate(self.allocation.x, self.allocation.y)
+ width = self.allocation.width
+ height = self.allocation.height
+ xradius = width/2
+ yradius = height/2
+ width -= self.line_width
+ height -= self.line_width
+ #
+ # TODO fetch speaker coordinates
+
+ # draw bubble tail if present
+ if self.tailpos != (0,0):
+ context.move_to(xradius-width/4, yradius)
+ context.line_to(self.tailpos[0], self.tailpos[1])
+ context.line_to(xradius+width/4, yradius)
+ context.set_line_width(self.line_width)
+ context.set_source_rgb(*xo_line_color)
+ context.stroke_preserve()
+
+ # bubble border
+ context.move_to(width-self.padding, 0.0)
+ context.line_to(self.padding, 0.0)
+ context.arc_negative(self.padding, self.padding, self.padding,
+ 3*pi/2, pi)
+ context.line_to(0.0, height-self.padding)
+ context.arc_negative(self.padding, height-self.padding, self.padding,
+ pi, pi/2)
+ context.line_to(width-self.padding, height)
+ context.arc_negative(width-self.padding, height-self.padding,
+ self.padding, pi/2, 0)
+ context.line_to(width, self.padding)
+ context.arc_negative(width-self.padding, self.padding, self.padding,
+ 0.0, -pi/2)
+ context.set_line_width(self.line_width)
+ context.set_source_rgb(*xo_line_color)
+ context.stroke_preserve()
+ context.set_source_rgb(*xo_fill_color)
+ context.fill()
+
+ # bubble painting. Redrawing the inside after the tail will combine
+ if self.tailpos != (0,0):
+ context.move_to(xradius-width/4, yradius)
+ context.line_to(self.tailpos[0], self.tailpos[1])
+ context.line_to(xradius+width/4, yradius)
+ context.set_line_width(self.line_width)
+ context.set_source_rgb(*xo_fill_color)
+ context.fill()
+
+ # draw/write text
+ context.set_source_rgb(1.0, 1.0, 1.0)
+ pangoctx = pangocairo.CairoContext(context)
+ self._text_layout.set_markup(self.__label)
+ text_size = self._text_layout.get_pixel_size()
+ pangoctx.move_to(
+ int((self.allocation.width-text_size[0])/2),
+ int(self.line_width+self.imgsize[1]+self.padding/2))
+ pangoctx.show_layout(self._text_layout)
+
+ # create a new cairo surface to place the image on
+ #surface = cairo.ImageSurface(0,x,y)
+ # create a context to the new surface
+ #ct = cairo.Context(surface)
+
+ # paint image
+ context.set_source_pixbuf(
+ self.pixbuf,
+ int((self.allocation.width-self.imgsize[0])/2),
+ int(self.line_width+self.padding/2))
+
+ context.paint()
+
+ # work done. Be kind to next cairo widgets and reset matrix.
+ context.identity_matrix()
+
+ def do_realize(self):
+ """ Setup gdk window creation. """
+ self.set_flags(gtk.REALIZED | gtk.NO_WINDOW)
+ self.window = self.get_parent_window()
+ if not self._no_expose:
+ self.__exposer = self.connect_after("expose-event", \
+ self.__on_expose)
+
+ def __on_expose(self, widget, event):
+ """Redraw event callback."""
+ ctx = self.window.cairo_create()
+
+ self.draw_with_context(ctx)
+
+ return True
+
+ def _set_label(self, value):
+ """Sets the label and flags the widget to be redrawn."""
+ self.__label = "<b>%s</b>"%value
+ if not self.parent:
+ return
+ ctx = self.parent.window.cairo_create()
+ pangoctx = pangocairo.CairoContext(ctx)
+ self._text_layout = pangoctx.create_layout()
+ self._text_layout.set_markup(value)
+ del pangoctx, ctx#, surf
+
+ def do_size_request(self, requisition):
+ """Fill requisition with size occupied by the widget."""
+ ctx = self.parent.window.cairo_create()
+ pangoctx = pangocairo.CairoContext(ctx)
+ self._text_layout = pangoctx.create_layout()
+ self._text_layout.set_markup(self.__label)
+
+ width, height = self._text_layout.get_pixel_size()
+
+ max_width = width
+ if self.imgsize[0] > width:
+ max_width = self.imgsize[0]
+ requisition.width = int(2*self.padding+max_width)
+
+ requisition.height = int(2*self.padding+height+self.imgsize[1])
+
+ def do_size_allocate(self, allocation):
+ """Save zone allocated to the widget."""
+ self.allocation = allocation
+
+ def _get_label(self):
+ """Getter method for the label property"""
+ return self.__label[3:-4]
+
+ def _set_no_expose(self, value):
+ """setter for no_expose property"""
+ self._no_expose = value
+ if not (self.flags() and gtk.REALIZED):
+ return
+
+ if self.__exposer and value:
+ self.parent.disconnect(self.__exposer)
+ self.__exposer = None
+ elif (not self.__exposer) and (not value):
+ self.__exposer = self.parent.connect_after("expose-event",
+ self.__on_expose)
+
+ def _get_no_expose(self):
+ """getter for no_expose property"""
+ return self._no_expose
+
+ no_expose = property(fset=_set_no_expose, fget=_get_no_expose,
+ doc="Whether the widget should handle exposition events or not.")
+
+ label = property(fget=_get_label, fset=_set_label,
+ doc="Text label which is to be painted on the top of the widget")
+
+gobject.type_register(TextBubbleWImg)
+
+
class Rectangle(gtk.Widget):
"""
A CanvasDrawableWidget drawing a rectangle over a specified widget.
diff --git a/tutorius/properties.py b/tutorius/properties.py
index 01cd2c0..cc76748 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -25,7 +25,9 @@ from copy import copy, deepcopy
from .constraints import Constraint, \
UpperLimitConstraint, LowerLimitConstraint, \
MaxSizeConstraint, MinSizeConstraint, \
- ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint
+ ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint, \
+ ResourceConstraint
+
from .propwidgets import PropWidget, \
StringPropWidget, \
UAMPropWidget, \
@@ -99,6 +101,21 @@ class TPropContainer(object):
except AttributeError:
return object.__setattr__(self, name, value)
+ def replace_property(self, prop_name, new_prop):
+ """
+ Changes the content of a property. This is done in order to support
+ the insertion of executable properties in the place of a portable
+ property. The typical exemple is that a resource property needs to
+ be changed into a file property with the correct file name, since the
+ installation location will be different on every platform.
+
+ @param prop_name The name of the property to be changed
+ @param new_prop The new property to insert
+ @raise AttributeError of the mentionned property doesn't exist
+ """
+ props = object.__getattribute__(self, "_props")
+ props.__setitem__(prop_name, new_prop)
+
def get_properties(self):
"""
Return the list of property names.
@@ -289,6 +306,37 @@ class TFileProperty(TutoriusProperty):
self.default = self.validate(path)
+class TResourceProperty(TutoriusProperty):
+ """
+ Represents a resource in the tutorial. A resource is a file with a specific
+ name that exists under the tutorials folder. It is distributed alongside the
+ tutorial itself.
+
+ When the system encounters a resource, it knows that it refers to a file in
+ the resource folder and that it should translate this resource name to an
+ absolute file name before it is executed.
+
+ E.g. An image is added to a tutorial in an action. On doing so, the creator
+ adds a resource to the tutorial, then saves its name in the resource
+ property of that action. When this tutorial is executed, the Engine
+ replaces all the TResourceProperties inside the action by their equivalent
+ TFileProperties with absolute paths, so that they can be used on any
+ machine.
+ """
+ def __init__(self, resource_name=""):
+ """
+ Creates a new resource pointing to an existing resource.
+
+ @param resource_name The file name of the resource (should be only the
+ file name itself, no directory information)
+ """
+ TutoriusProperty.__init__(self)
+ self.type = "resource"
+
+ self.resource_cons = ResourceConstraint()
+
+ self.default = self.validate("")
+
class TEnumProperty(TutoriusProperty):
"""
Represents a value in a given enumeration. This means that the value will
diff --git a/tutorius/translator.py b/tutorius/translator.py
new file mode 100644
index 0000000..4f29078
--- /dev/null
+++ b/tutorius/translator.py
@@ -0,0 +1,200 @@
+# 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
+import logging
+import copy as copy_module
+
+logger = logging.getLogger("ResourceTranslator")
+
+from .properties import *
+from .vault import Vault
+from .dbustools import save_args
+
+class ResourceTranslator(object):
+ """
+ Handles the conversion of resource properties into file
+ properties before action execution. This class works as a decorator
+ to the ProbeManager class, as it is meant to be a transparent layer
+ before sending the action to execution.
+
+ An architectural note : every different type of translation should have its
+ own method (translate_resource, etc...), and this function must be called
+ from the translate method, under the type test. The translate_* method
+ must take in the input property and give the output property that should
+ replace it.
+ """
+
+ def __init__(self, probe_manager, tutorial_id):
+ """
+ Creates a new ResourceTranslator for the given tutorial. This
+ translator is tasked with replacing resource properties of the
+ incoming action into actually usable file properties pointing
+ to the correct resource file. This is done by querying the vault
+ for all the resources and creating a new file property from the
+ returned path.
+
+ @param probe_manager The probe manager to decorate
+ @param tutorial_id The ID of the current tutorial
+ """
+ self._probe_manager = probe_manager
+ self._tutorial_id = tutorial_id
+
+ def translate_resource(self, res_value):
+ """
+ Replace the TResourceProperty in the container by their
+ runtime-defined file equivalent. Since the resources are stored
+ in a relative manner in the vault and that only the Vault knows
+ which is the current folder for the current tutorial, it needs
+ to transform the resource identifier into the absolute path for
+ the process to be able to use it properly.
+
+ @param res_prop The resource property's value to be translated
+ @return The TFileProperty corresponding to this resource, containing
+ an absolute path to the resource
+ """
+ # We need to replace the resource by a file representation
+ filepath = Vault.get_resource_path(self._tutorial_id, res_value)
+ logger.debug("ResourceTranslator :: Matching resource %s to file %s" % (res_value, filepath))
+
+ # Create the new file representation
+ file_prop = TFileProperty(filepath)
+
+ return file_prop
+
+ def translate(self, prop_container):
+ """
+ Applies the required translation to be able to send the container to an
+ executing endpoint (like a Probe). For each type of property that
+ requires it, there is translation function that will take care of
+ mapping the property to its executable form.
+
+ This function does not return anything, but its post-condition is
+ that all the properties of the input container have been replaced
+ by their corresponding executable versions.
+
+ An example of translation is taking a resource (a relative path to
+ a file under the tutorial folder) and transforming it into a file
+ (a full absolute path) to be able to load it when the activity receives
+ the action.
+
+ @param prop_container The property container in which we want to
+ replace all the resources for file properties and
+ to recursively do so for addon and addonlist
+ properties.
+ """
+ for propname in prop_container.get_properties():
+ prop_value = getattr(prop_container, propname)
+ prop_type = getattr(type(prop_container), propname).type
+
+ # If the property is a resource, then we need to query the
+ # vault to create its correspondent
+ if prop_type == "resource":
+ # Apply the translation
+ file_prop = self.translate_resource(prop_value)
+ # Set the property with the new value
+ prop_container.replace_property(propname, file_prop)
+
+ # If the property is an addon, then its value IS a
+ # container too - we need to translate it
+ elif prop_type == "addon":
+ # Translate the sub properties
+ self.translate(prop_value)
+
+ # If the property is an addon list, then we need to translate all
+ # the elements of the list
+ elif prop_type == "addonlist":
+ # Now, for each sub-container in the list, we will apply the
+ # translation processing. This is done by popping the head of
+ # the list, translating it and inserting it back at the end.
+ for index in range(0, len(prop_value)):
+ # Pop the head of the list
+ container = prop_value[0]
+ del prop_value[0]
+ # Translate the sub-container
+ self.translate(container)
+
+ # Put the value back in the list
+ prop_value.append(container)
+ # Change the list contained in the addonlist property, since
+ # we got a copy of the list when requesting it
+ prop_container.replace_property(propname, prop_value)
+
+ ###########################################################################
+ ### ProbeManager interface for decorator
+
+ # Unchanged functions
+ def setCurrentActivity(self, activity_id):
+ self._probe_manager.currentActivity = activity_id
+
+ def getCurrentActivity(self):
+ return self._probe_manager.currentActivity
+
+ currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
+ def attach(self, activity_id):
+ self._probe_manager.attach(activity_id)
+
+ def detach(self, activity_id):
+ self._probe_manager.detach(activity_id)
+
+ def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb):
+ return self._probe_manager.subscribe(event, notification_cb, event_subscribed_cb, error_cb)
+
+ def unsubscribe(self, address):
+ return self._probe_manager.unsubscribe(address)
+
+ def register_probe(self, process_name, unique_id):
+ self._probe_manager.register_probe(process_name, unique_id)
+
+ def unregister_probe(self, unique_id):
+ self._probe_manager.unregister_probe(unique_id)
+
+ def get_registered_probes_list(self, process_name=None):
+ return self._probe_manager.get_registered_probes_list(process_name)
+
+ ###########################################################################
+
+ def action_installed(self, action_installed_cb, address):
+ # Callback to the upper layers to inform them that the action
+ # was installed
+ action_installed_cb(address)
+
+ def action_install_error(self, install_error_cb, old_action, exception):
+ # Warn the upper layer that the installation failed
+ install_error_cb(old_action, exception)
+
+ # Decorated functions
+ def install(self, action, action_installed_cb, error_cb):
+ # Make a new copy of the action that we want to install,
+ # because translate() changes the action and we
+ # don't want to modify the caller's action representation
+ new_action = copy_module.deepcopy(action)
+ # Execute the replacement
+ self.translate(new_action)
+
+ # Send the new action to the probe manager
+ self._probe_manager.install(new_action, save_args(self.action_installed, action_installed_cb),
+ save_args(self.action_install_error, error_cb, new_action))
+
+ def update(self, action_address, newaction):
+ translated_new_action = copy_module.deepcopy(newaction)
+ self.translate(translated_new_action)
+
+ self._probe_manager.update(action_address, translated_new_action, block)
+
+ def uninstall(self, action_address):
+ self._probe_manager.uninstall(action_address)
+
diff --git a/tutorius/vault.py b/tutorius/vault.py
index a3b84a4..1c1e33c 100644
--- a/tutorius/vault.py
+++ b/tutorius/vault.py
@@ -251,26 +251,25 @@ class Vault(object):
addition_flag = False
# Check if at least one keyword of the list is present
for key in keyword:
- for value in metadata_dictionnary.values():
- if isinstance(value, str) == True:
- if value.lower().count(key.lower()) > 0:
- addition_flag = True
- else:
+ if key != None:
+ for value in metadata_dictionnary.values():
+ if isinstance(value, str):
+ if value.lower().count(key.lower()) > 0:
+ addition_flag = True
# Check one layer of depth in the metadata to find the keyword
# (for exemple, related activites are a dictionnary stocked
- # in a value of the main dictionnary)
- if isinstance(value, dict) == True:
+ # in a value of the main dictionnary)
+ elif isinstance(value, dict):
for inner_key, inner_value in value.items():
- if isinstance(inner_value, str) == True and isinstance(inner_key, str) == True:
- if inner_value.lower().count(key.lower()) > 0 or inner_key.count(key.lower()) > 0:
- addition_flag = True
+ if isinstance(inner_value, str) and isinstance(inner_key, str) and (inner_value.lower().count(key.lower()) > 0 or inner_key.count(key.lower()) > 0):
+ addition_flag = True
# Filter tutorials for related activities
if relatedActivityNames != []:
addition_flag = False
# Check if at least one element of the list is present
for related in relatedActivityNames:
- if related.lower() in related_act_dictionnary.keys():
+ if related != None and related.lower() in related_act_dictionnary.keys():
addition_flag = True
# Filter tutorials for categories
@@ -278,9 +277,8 @@ class Vault(object):
addition_flag = False
# Check if at least one element of the list is present
for cat in category:
- if metadata_dictionnary.has_key(INI_CATEGORY_PROPERTY):
- if metadata_dictionnary[INI_CATEGORY_PROPERTY].lower() == cat.lower():
- addition_flag = True
+ if cat != None and metadata_dictionnary.has_key(INI_CATEGORY_PROPERTY) and metadata_dictionnary[INI_CATEGORY_PROPERTY].lower() == cat.lower():
+ addition_flag = True
# Add this dictionnary to tutorial list if it has not been filtered
if addition_flag == True:
@@ -524,6 +522,7 @@ class Vault(object):
tutorial_path = bundler.get_tutorial_path(tutorial_guid)
# Check if the resource file exists
file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id)
+ logger.debug("Vault :: Assigning resource %s to file %s "%(resource_id, file_path))
if os.path.isfile(file_path):
return file_path
else: