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-11-16 01:04:44 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-11-16 01:04:44 (GMT)
commit0c0ed009fd711e001d600d47bfa1db7d7fb3d23b (patch)
tree49eef0c445efe1b98e6c22eccee6e09724d7994f
parentfe23113f46d8264d49acb1bd5214c03fb706189f (diff)
parent857aff7e1c1694a819c0dbc9b9103ef2206699e6 (diff)
Merge branch 'master' of git://git.sugarlabs.org/tutorius/mainline
-rw-r--r--tests/enginetests.py116
-rw-r--r--tests/probetests.py132
-rw-r--r--tests/skip1
-rw-r--r--tests/storetests.py12
-rw-r--r--tutorius/TProbe.py140
-rw-r--r--tutorius/creator.py173
-rw-r--r--tutorius/engine.py141
-rw-r--r--tutorius/service.py65
-rw-r--r--tutorius/viewer.py119
9 files changed, 610 insertions, 289 deletions
diff --git a/tests/enginetests.py b/tests/enginetests.py
new file mode 100644
index 0000000..30d68de
--- /dev/null
+++ b/tests/enginetests.py
@@ -0,0 +1,116 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Erick Lavoie <erick.lavoie@gmail.com>
+#
+# 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
+"""
+Engine Tests
+
+
+
+Usage of actions and event filters is tested, but not the concrete actions
+and event filters. Those are in their separate test module
+
+"""
+
+import unittest
+
+from sugar.tutorius.tutorial import Tutorial
+from sugar.tutorius.engine import TutorialRunner
+from sugar.tutorius.filters import EventFilter
+
+from actiontests import CountAction
+
+class MockProbeMgr(object):
+ def __init__(self):
+ self.action = None
+ self.event = None
+ self.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):
+ self.action = action
+
+ def update(self, action, newaction, block=False):
+ self.action = newaction
+
+ def uninstall(self, action, block=False):
+ self.action = None
+
+ def subscribe(self, event, callback):
+ self.event = event
+ self.cB = callback
+ self.event.install_handlers(callback)
+ return str(event)
+
+ def unsubscribe(self, address):
+ self.event = None
+
+class MockEvent(EventFilter):
+ pass
+
+
+
+class TutorialRunnerTest(unittest.TestCase):
+ """
+ This class needs to test the TutorialRunner
+ """
+ def setUp(self):
+ self.pM = MockProbeMgr()
+
+
+ def tearDown(self):
+ self.pM = None
+
+ # Basic interface cases
+ def testOneStateTutorial(self):
+ tutorial = Tutorial("TutorialRunner")
+ state_name = tutorial.add_state()
+ tutorial.update_transition(Tutorial.INITIAL_TRANSITION_NAME,
+ None, state_name)
+ event = MockEvent()
+ tutorial.add_transition(state_name, (event, Tutorial.END))
+
+ runner = TutorialRunner(tutorial, self.pM)
+ runner.start()
+
+ assert runner._state == state_name, "Current 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 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")
+ runner = TutorialRunner(tutorial, self.pM)
+ runner.start()
+
+ assert runner._state == Tutorial.END, "Current state is: %s"%runner._state
+ assert self.pM.action == None
+ assert self.pM.event == None
+
+ # Error cases
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/probetests.py b/tests/probetests.py
index e1a587b..59072e5 100644
--- a/tests/probetests.py
+++ b/tests/probetests.py
@@ -47,7 +47,7 @@ class MockAddon(Action):
i = TIntProperty(0)
s = TStringProperty("test")
- def do(self):
+ def do(self, **kwargs):
global message_box
message_box = (self.i, self.s)
@@ -66,15 +66,20 @@ class MockAddon(Action):
fake_addon_cache["MockAddon"] = MockAddon
class MockActivity(object):
- pass
+ def get_bundle_id(self):
+ return "localhost.unittest.ProbeTest"
+
+ def get_id(self):
+ return "unique_id_1"
+
class MockProbeProxy(object):
_MockProxyCache = {}
- def __new__(cls, activityName):
+ def __new__(cls, activityName, unique_id):
#For testing, use only one instance per activityName
return cls._MockProxyCache.setdefault(activityName, super(MockProbeProxy, cls).__new__(cls))
- def __init__(self, activityName):
+ def __init__(self, activityName, unique_id):
"""
Constructor
@param activityName unique activity id. Must be a valid dbus bus name.
@@ -153,6 +158,12 @@ class MockSessionBus(object):
old_SessionBus = dbus.SessionBus
+class MockServiceProxy(object):
+ def register_probe(self, process_name, unique_id):
+ pass
+ def unregister_probe(self, unique_id):
+ pass
+
###########################################################################
# Begin Test Cases
###########################################################################
@@ -170,7 +181,7 @@ class ProbeTest(unittest.TestCase):
#Setup the activity and probe
self.activity = MockActivity()
- self.probe = TProbe("localhost.unittest.ProbeTest", self.activity)
+ self.probe = TProbe(self.activity, MockServiceProxy())
#Override the eventOccured on the Probe...
self.old_eO = self.probe.eventOccured
@@ -287,67 +298,49 @@ class ProbeManagerTest(unittest.TestCase):
MockProbeProxy._MockProxyCache = {}
self.probeManager = ProbeManager(proxy_class=MockProbeProxy)
- def test_attach(self):
- #ErrorCase: Set currentActivity to unattached activity
- #Attempt to set to a non existing activity
- try:
- self.probeManager.currentActivity = "act1"
- assert False, "Exception expected"
- except RuntimeError, e:
- pass
-
- #Attach an activity
- self.probeManager.attach("act1")
-
- #Should have been created
- assert "act1" in MockProbeProxy._MockProxyCache.keys(), "Proxy not created"
-
- #ErrorCase: Attach multiple times to same activity
- #Try to attach again
- self.assertRaises(RuntimeWarning, self.probeManager.attach, "act1")
-
- #Set current activity should work
- self.probeManager.currentActivity = "act1"
-
- #TODO Fill in the alive/notalive behavior at creation time once
- # it is fixed in the ProbeManager
-
- def test_detach(self):
- #attach an activity
- self.probeManager.attach("act1")
- self.probeManager.currentActivity = "act1"
- act1 = MockProbeProxy("act1")
-
- #Now we detach
- self.probeManager.detach("act1")
- assert act1.MockAlive == False, "ProbeProxy should have been detached"
- assert self.probeManager.currentActivity is None, "Current activity should be None"
-
- #Attempt to detach again, should do nothing
- #ErrorCase: detach already detached (currently silent fail)
- self.probeManager.detach("act1")
-
- #Now, attach 2 activities
- self.probeManager.attach("act2")
- self.probeManager.attach("act3")
- act2 = MockProbeProxy("act2")
- act3 = MockProbeProxy("act3")
-
- self.probeManager.currentActivity = "act2"
-
- assert act2.MockAlive and act3.MockAlive, "Both ProbeProxy instances should be alive"
+ def test_register_probe(self):
+ assert len(self.probeManager.get_registered_probes_list()) == 0
+
+ self.probeManager.register_probe("act1", "unique_id_1")
+ assert len(self.probeManager.get_registered_probes_list()) == 1
+ assert len(self.probeManager.get_registered_probes_list("act1")) == 1
+ assert self.probeManager.get_registered_probes_list()[0][0] == "unique_id_1"
+
+ self.probeManager.register_probe("act2","unique_id_2")
+ assert len(self.probeManager.get_registered_probes_list()) == 2
+ assert len(self.probeManager.get_registered_probes_list("act1")) == 1
+ assert self.probeManager.get_registered_probes_list("act1")[0][0] == "unique_id_1"
+ assert len(self.probeManager.get_registered_probes_list("act2")) == 1
+ assert self.probeManager.get_registered_probes_list("act2")[0][0] == "unique_id_2"
+
+ def test_register_multiple_probes(self):
+ assert len(self.probeManager.get_registered_probes_list()) == 0
+
+ self.probeManager.register_probe("act1", "unique_id_1")
+ self.probeManager.register_probe("act1","unique_id_2")
+ assert len(self.probeManager.get_registered_probes_list()) == 2
+ assert len(self.probeManager.get_registered_probes_list("act1")) == 2
+ assert self.probeManager.get_registered_probes_list("act1")[0][0] == "unique_id_1"
+ assert self.probeManager.get_registered_probes_list("act1")[1][0] == "unique_id_2"
+
+ def test_unregister_probe(self):
+ assert len(self.probeManager.get_registered_probes_list()) == 0
+ self.probeManager.register_probe("act1", "unique_id_1")
+ self.probeManager.register_probe("act1","unique_id_2")
+
+ self.probeManager.unregister_probe("unique_id_1")
+ assert len(self.probeManager.get_registered_probes_list("act1")) == 1
+ assert self.probeManager.get_registered_probes_list("act1")[0][0] == "unique_id_2"
- #Detach the not active activity
- self.probeManager.detach("act3")
- #Check the statuses
- assert act2.MockAlive and not act3.MockAlive, "Only act2 should be alive"
- assert self.probeManager.currentActivity == "act2", "act2 should not have failed"
+ self.probeManager.unregister_probe("unique_id_2")
+ assert len(self.probeManager.get_registered_probes_list("act1")) == 0
+ assert self.probeManager.get_registered_probes_list("act1") == []
def test_actions(self):
- self.probeManager.attach("act1")
- self.probeManager.attach("act2")
- act1 = MockProbeProxy("act1")
- act2 = MockProbeProxy("act2")
+ self.probeManager.register_probe("act1", "unique_id_1")
+ self.probeManager.register_probe("act2", "unique_id_2")
+ act1 = self.probeManager.get_registered_probes_list("act1")[0][1]
+ act2 = self.probeManager.get_registered_probes_list("act2")[0][1]
ad1 = MockAddon()
#ErrorCase: install, update, uninstall without currentActivity
@@ -376,10 +369,10 @@ class ProbeManagerTest(unittest.TestCase):
assert act1.MockAction is None, "Action should be uninstalled"
def test_events(self):
- self.probeManager.attach("act1")
- self.probeManager.attach("act2")
- act1 = MockProbeProxy("act1")
- act2 = MockProbeProxy("act2")
+ self.probeManager.register_probe("act1", "unique_id_1")
+ self.probeManager.register_probe("act2", "unique_id_2")
+ act1 = self.probeManager.get_registered_probes_list("act1")[0][1]
+ act2 = self.probeManager.get_registered_probes_list("act2")[0][1]
ad1 = MockAddon()
ad2 = MockAddon()
@@ -405,12 +398,13 @@ 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
- self.mockObj = MockProxyObject("unittest.TestCase", "/tutorius/Probe")
- self.probeProxy = ProbeProxy("unittest.TestCase")
+ self.mockObj = MockProxyObject("unittest.TestCase", "/tutorius/Probe/unique_id_1")
+ self.probeProxy = ProbeProxy("unittest.TestCase", "unique_id_1")
def tearDown(self):
dbus.SessionBus = old_SessionBus
diff --git a/tests/skip b/tests/skip
index 3868383..028ecaf 100644
--- a/tests/skip
+++ b/tests/skip
@@ -2,3 +2,4 @@ utils.py
run-tests.py
overlaytests.py
viewer.py
+coretests.py
diff --git a/tests/storetests.py b/tests/storetests.py
index 0c36973..3f1b73c 100644
--- a/tests/storetests.py
+++ b/tests/storetests.py
@@ -31,13 +31,11 @@ class StoreProxyTest(unittest.TestCase):
def tearDown(self):
pass
- @catch_unimplemented
def test_get_categories(self):
categories = self.store.get_categories()
assert isinstance(categories, list), "categories should be a list"
- @catch_unimplemented
def test_get_tutorials(self):
self.store.get_tutorials()
@@ -46,17 +44,14 @@ class StoreProxyTest(unittest.TestCase):
assert isinstance(version_dict, dict)
- @catch_unimplemented
def test_download_tutorial(self):
tutorial = self.store.download_tutorial(g_other_id)
assert tutorial is not None
- @catch_unimplemented
def test_login(self):
assert self.store.login("benoit.tremblay1@gmail.com", "tutorius12")
- @catch_unimplemented
def test_register_new_user(self):
random_num = str(random.randint(0, 999999999))
user_info = {
@@ -69,29 +64,24 @@ class StoreProxyTest(unittest.TestCase):
class StoreProxyLoginTest(unittest.TestCase):
- @catch_unimplemented
def setUp(self):
self.store = StoreProxy("http://bobthebuilder.mine.nu/tutorius/en-US/tutorius")
self.store.login("nobody@mozilla.org", "tutorius12")
- @catch_unimplemented
def tearDown(self):
session_id = self.store.get_session_id()
if session_id is not None:
self.store.close_session()
- @catch_unimplemented
def test_get_session_id(self):
session_id = self.store.get_session_id()
assert session_id is not None
- @catch_unimplemented
def test_rate(self):
assert self.store.rate(5, g_tutorial_id)
- @catch_unimplemented
def test_publish(self):
# TODO : We need to send in a real tutorial loaded from
# the Vault
@@ -108,7 +98,6 @@ class StoreProxyLoginTest(unittest.TestCase):
}
assert self.store.publish('This should be a real tutorial...', tutorial_info) != -1
- @catch_unimplemented
def test_unpublish(self):
assert self.store.unpublish(g_tutorial_id)
@@ -118,7 +107,6 @@ class StoreProxyLoginTest(unittest.TestCase):
def test_republish(self):
assert self.store.publish(None, None, g_tutorial_id)
- @catch_unimplemented
def test_update_published_tutorial(self):
# TODO : Run these tests with files from the Vault
#self.store.publish([g_tutorial_id, 'Fake tutorial'])
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
index dbab86a..0c79690 100644
--- a/tutorius/TProbe.py
+++ b/tutorius/TProbe.py
@@ -38,45 +38,65 @@ class TProbe(dbus.service.Object):
a DBUS Interface.
"""
- def __init__(self, activity_name, activity):
+ def __init__(self, activity, service_proxy=None):
"""
Create and register a TProbe for an activity.
- @param activity_name unique activity_id
@param activity activity reference, must be a gtk container
+ @param service_proxy A Service proxy object to do the registering
"""
- LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid())
- LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default()))
- LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth()))
# Moving the ObjectStore assignment here, in the meantime
# the reference to the activity shouldn't be share as a
# global variable but passed by the Probe to the objects
# that requires it
self._activity = activity
+
+ if service_proxy == None:
+ from .service import ServiceProxy
+
+ self._service_proxy = service_proxy or ServiceProxy()
ObjectStore().activity = activity
- self._activity_name = activity_name
+ self._activity_name = activity.get_bundle_id()
+ self._unique_id = activity.get_id()
+
+ LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", self._activity_name, os.getpid())
+ LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default()))
+ LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth()))
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")
+ self._name2 = dbus.service.BusName(self._activity_name, self._session_bus)
+ dbus.service.Object.__init__(self, self._session_bus, "/tutorius/Probe/"+str(self._unique_id))
# Add the dictionary we will use to store which actions and events
# are known
self._installedActions = {}
self._subscribedEvents = {}
+ 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):
"""
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()
+ def stop(self):
+ """
+ Clean up the probe when finished. Should be called just
+ before a process ends
+ """
+ from .service import ServiceProxy
+ LOGGER.debug("TProbe :: unregistering '%s' with unique_id '%s'", self._activity_name, self._unique_id)
+ ServiceProxy().unregister_probe(self._unique_id)
+
@dbus.service.method("org.tutorius.ProbeInterface",
in_signature='s', out_signature='')
def registered(self, service):
@@ -234,16 +254,17 @@ class ProbeProxy:
It provides an object interface to the TProbe, which requires pickled
strings, across a DBus communication.
"""
- def __init__(self, activityName):
+ def __init__(self, activityName, unique_id):
"""
Constructor
- @param activityName unique activity id. Must be a valid dbus bus name.
+ @param activityName generic activity name. Must be a valid dbus bus name.
+ @param unique_id unique id specific to an instance of an activity
"""
LOGGER.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid())
LOGGER.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default()))
LOGGER.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth()))
bus = dbus.SessionBus()
- self._object = bus.get_object(activityName, "/tutorius/Probe")
+ self._object = bus.get_object(activityName, "/tutorius/Probe/"+str(unique_id))
self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface")
self._actions = {}
@@ -397,7 +418,7 @@ class ProbeProxy:
return_cb=save_args(self.__clear_event, address),
block=block)
else:
- LOGGER.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address)
+ LOGGER.debug("ProbeProxy :: unsubscribe address %s failed : not registered", address)
def detach(self, block=False):
"""
@@ -418,16 +439,22 @@ class ProbeManager(object):
For now, it only handles one at a time, though.
Actually it doesn't do much at all. But it keeps your encapsulation happy
"""
+ _LOGGER = logging.getLogger("sugar.tutorius.ProbeManager")
+
def __init__(self, proxy_class=ProbeProxy):
"""Constructor
@param proxy_class Class to use for creating Proxies to activities.
The class should support the same interface as ProbeProxy. Exists
to make this class unit-testable by replacing the Proxy with a mock
"""
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
self._ProxyClass = proxy_class
self._probes = {}
self._current_activity = None
+ ProbeManager._LOGGER.debug("__init__()")
+
def setCurrentActivity(self, activity_id):
if not activity_id in self._probes:
raise RuntimeError("Activity not attached")
@@ -437,23 +464,6 @@ class ProbeManager(object):
return self._current_activity
currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
- def attach(self, activity_id):
- if activity_id in self._probes:
- raise RuntimeWarning("Activity already attached")
-
- self._probes[activity_id] = self._ProxyClass(activity_id)
- #TODO what do we do with this? Raise something?
- if self._probes[activity_id].isAlive():
- print "Alive!"
- else:
- print "FAil!"
-
- def detach(self, activity_id):
- if activity_id in self._probes:
- probe = self._probes.pop(activity_id)
- probe.detach()
- if self._current_activity == activity_id:
- self._current_activity = None
def install(self, action, block=False):
"""
@@ -463,7 +473,7 @@ class ProbeManager(object):
@return None
"""
if self.currentActivity:
- return self._probes[self.currentActivity].install(action, block)
+ return self._first_proxy(self.currentActivity).install(action, block)
else:
raise RuntimeWarning("No activity attached")
@@ -476,7 +486,7 @@ class ProbeManager(object):
@return None
"""
if self.currentActivity:
- return self._probes[self.currentActivity].update(action, newaction, block)
+ return self._first_proxy(self.currentActivity).update(action, newaction, block)
else:
raise RuntimeWarning("No activity attached")
@@ -487,7 +497,7 @@ class ProbeManager(object):
@param block Force a synchroneous dbus call if True
"""
if self.currentActivity:
- return self._probes[self.currentActivity].uninstall(action, block)
+ return self._first_proxy(self.currentActivity).uninstall(action, block)
else:
raise RuntimeWarning("No activity attached")
@@ -499,7 +509,7 @@ class ProbeManager(object):
@return address identifier used for unsubscribing
"""
if self.currentActivity:
- return self._probes[self.currentActivity].subscribe(event, callback)
+ return self._first_proxy(self.currentActivity).subscribe(event, callback)
else:
raise RuntimeWarning("No activity attached")
@@ -510,7 +520,67 @@ class ProbeManager(object):
@return None
"""
if self.currentActivity:
- return self._probes[self.currentActivity].unsubscribe(address)
+ return self._first_proxy(self.currentActivity).unsubscribe(address)
else:
raise RuntimeWarning("No activity attached")
+ def register_probe(self, process_name, unique_id):
+ """ Adds a probe to the known probes, to be used by a tutorial.
+
+ A generic name for a process (like an Activity) is passed
+ so that the execution of a tutorial will use that generic
+ name. However, a unique id is also passed to differentiate
+ between many instances of the same process.
+
+ @param process_name The generic name of a process
+ @param unique_id The unique identification associated to this
+ process
+ """
+ ProbeManager._LOGGER.debug("register_probe(%s,%s)", process_name, unique_id)
+ if process_name not in self._probes:
+ self._probes[process_name] = [(unique_id,self._ProxyClass(process_name, unique_id))]
+ else:
+ self._probes[process_name].append((unique_id,self._ProxyClass(process_name, unique_id)))
+
+
+ def unregister_probe(self, unique_id):
+ """ Remove a probe from the known probes.
+
+ @param unique_id The unique identification associated to this
+ process
+ """
+ ProbeManager._LOGGER.debug("unregister_probe(%s)", unique_id)
+ for process_name, proxies in self._probes.items():
+ for id, proxy in proxies:
+ if unique_id == id:
+ proxy.detach()
+ proxies.remove((id,proxy))
+ if len(proxies) == 0:
+ self._probes.pop(process_name)
+
+ def get_registered_probes_list(self, process_name=None):
+ if process_name == None:
+ probe_list = []
+ for probes in self._probes.itervalues():
+ probe_list.extend(probes)
+ return probe_list
+ else:
+ if process_name in self._probes:
+ return self._probes[process_name]
+ else:
+ return []
+
+
+
+ def _first_proxy(self, process_name):
+ """
+ Returns the oldest probe connected under the process_name
+ @param process_name The generic process name under which the probe
+ is connected
+ """
+ if process_name in self._probes:
+ return self._probes[process_name][0][1]
+ else:
+ raise RuntimeWarning("No activity attached under '%s'", process_name)
+
+
diff --git a/tutorius/creator.py b/tutorius/creator.py
index c477056..f59f320 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -26,14 +26,15 @@ import gtk.glade
import gobject
from gettext import gettext as T
+import uuid
import os
-from sugar.graphics import icon
-import copy
+from sugar.graphics import icon, style
from . import overlayer, gtkutils, actions, vault, properties, addon
from . import filters
from .services import ObjectStore
-from .core import Tutorial, FiniteStateMachine, State
+from .core import State
+from .tutorial import Tutorial
from . import viewer
class Creator(object):
@@ -50,10 +51,11 @@ class Creator(object):
"""
self._activity = activity
if not tutorial:
- self._tutorial = FiniteStateMachine('Untitled')
- self._state = State(name='INIT')
- self._tutorial.add_state(self._state)
- self._state_counter = 1
+ self._tutorial = Tutorial('Untitled')
+ self._state = self._tutorial.add_state()
+ self._tutorial.update_transition(
+ transition_name=self._tutorial.INITIAL_TRANSITION_NAME,
+ new_state=self._state)
else:
self._tutorial = tutorial
# TODO load existing tutorial; unused yet
@@ -110,30 +112,17 @@ class Creator(object):
"""
Removes the first instance of specified action from the tutorial.
- @param action: the action object to remove from the tutorial
+ @param action: the action name
@returns: True if successful, otherwise False.
"""
- state = self._tutorial.get_state_by_name("INIT")
-
- while True:
- state_actions = state.get_action_list()
- for fsm_action in state_actions:
- if fsm_action is action:
- state.clear_actions()
- if state is self._state:
- fsm_action.exit_editmode()
- state_actions.remove(fsm_action)
- self.set_insertion_point(state.name)
- for keep_action in state_actions:
- state.add_action(keep_action)
- return True
-
- ev_list = state.get_event_filter_list()
- if ev_list:
- state = self._tutorial.get_state_by_name(ev_list[0][1])
- continue
-
+ action_obj = self._tutorial.get_action_dict(self._state)\
+ .get(action, None)
+ if not action_obj:
return False
+ action_obj.exit_editmode()
+ self._tutorial.delete_action(action)
+ self._overview.win.queue_draw()
+ return True
def delete_state(self):
"""
@@ -143,49 +132,25 @@ class Creator(object):
@returns: True if successful, otherwise False.
"""
- if not self._state.get_event_filter_list():
+ if self._state in (self._tutorial.INIT, self._tutorial.END):
# last state cannot be removed
return False
- state = self._tutorial.get_state_by_name("INIT")
- ev_list = state.get_event_filter_list()
- if state is self._state:
- next_state = self._tutorial.get_state_by_name(ev_list[0][1])
- self.set_insertion_point(next_state.name)
- self._tutorial.remove_state(state.name)
- self._tutorial.remove_state(next_state.name)
- next_state.name = "INIT"
- self._tutorial.add_state(next_state)
- return True
-
- # loop to repair links from deleted state
- while ev_list:
- next_state = self._tutorial.get_state_by_name(ev_list[0][1])
- if next_state is self._state:
- # the tutorial will flush the event filters. We'll need to
- # clear and re-add them.
- self._tutorial.remove_state(self._state.name)
- state.clear_event_filters()
- self._update_next_state(state, ev_list[0][0], next_state.get_event_filter_list()[0][1])
- for ev, next_state in ev_list:
- state.add_event_filter(ev, next_state)
-
- self.set_insertion_point(ev_list[0][1])
- return True
-
- state = next_state
- ev_list = state.get_event_filter_list()
- return False
+ remove_state = self._state
+ next_state = self._tutorial\
+ .get_following_states_dict(remove_state).keys()[0]
+ self.set_insertion_point(next_state)
+ return bool(self._tutorial.delete_state(remove_state))
def get_insertion_point(self):
- return self._state.name
+ return self._state
def set_insertion_point(self, state_name):
- for action in self._state.get_action_list():
+ for action in self._tutorial.get_action_dict(self._state).values():
action.exit_editmode()
- self._state = self._tutorial.get_state_by_name(state_name)
- self._overview.win.queue_draw()
- state_actions = self._state.get_action_list()
+
+ 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(
@@ -196,6 +161,8 @@ class Creator(object):
else:
self._propedit.action = None
+ self._overview.win.queue_draw()
+
def _evfilt_cb(self, menuitem, event):
"""
@@ -249,7 +216,7 @@ class Creator(object):
action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME]
action = addon.create(action_type)
action.enter_editmode()
- self._state.add_action(action)
+ 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)
@@ -283,31 +250,24 @@ class Creator(object):
else:
raise NotImplementedError()
- event_filters = self._state.get_event_filter_list()
+ event_filters = self._tutorial.get_transition_dict(self._state)
+
+ # if not at the end of tutorial
if event_filters:
- # linearize tutorial by inserting state
- new_state = State(name=str(self._state_counter))
- self._state_counter += 1
- self._state.clear_event_filters()
- for evt_filt, next_state in event_filters:
- new_state.add_event_filter(evt_filt, next_state)
- self._update_next_state(self._state, event, new_state.name)
- next_state = new_state.name
- # blocks are shifted, full redraw is necessary
- self._overview.win.queue_draw()
+ old_transition = event_filters.keys()[0]
+ new_state = self._tutorial.add_state(event_filters[old_transition])
+ self._tutorial.update_transition(transition_name=old_transition,
+ new_state=new_state)
+
else:
# append empty state only if edit inserting at end of linearized
# tutorial.
- self._update_next_state(self._state, event, str(self._state_counter))
- next_state = str(self._state_counter)
- new_state = State(name=str(self._state_counter))
- self._state_counter += 1
+ new_state = self._tutorial.add_state()
+ self._tutorial.add_transition(self._state, (event, new_state))
- self._state.add_event_filter(event, next_state)
- self._tutorial.add_state(new_state)
self._overview.win.queue_draw()
- self.set_insertion_point(new_state.name)
+ self.set_insertion_point(new_state)
def _action_refresh_cb(self, widget, evt, action):
"""
@@ -324,24 +284,27 @@ class Creator(object):
self._overview.win.queue_draw()
- def _cleanup_cb(self, *args):
+ def _cleanup_cb(self, *args, **kwargs):
"""
Quit editing and cleanup interface artifacts.
+
+ @param force: force quitting without saving.
"""
# undo actions so they don't persist through step editing
- for action in self._state.get_action_list():
+ for action in self._tutorial.get_action_dict(self._state).values():
action.exit_editmode()
- dialog = gtk.MessageDialog(
- parent=self._activity,
- flags=gtk.DIALOG_MODAL,
- type=gtk.MESSAGE_QUESTION,
- buttons=gtk.BUTTONS_YES_NO,
- message_format=T('Do you want to save before stopping edition?'))
- do_save = dialog.run()
- dialog.destroy()
- if do_save == gtk.RESPONSE_YES:
- self.save()
+ if kwargs.get(force, False):
+ dialog = gtk.MessageDialog(
+ parent=self._activity,
+ flags=gtk.DIALOG_MODAL,
+ type=gtk.MESSAGE_QUESTION,
+ buttons=gtk.BUTTONS_YES_NO,
+ message_format=T('Do you want to save before stopping edition?'))
+ do_save = dialog.run()
+ dialog.destroy()
+ if do_save == gtk.RESPONSE_YES:
+ self.save()
# remove UI remains
self._hlmask.covered = None
@@ -354,20 +317,24 @@ class Creator(object):
del self._activity._creator
def save(self, widget=None):
- if not self.tuto:
- dlg = TextInputDialog(self._activity,
+ if not self._guid:
+ self._guid = str(uuid.uuid1())
+ dlg = TextInputDialog(parent=self._overview.win,
text=T("Enter a tutorial title."),
field=T("Title"))
tutorialName = ""
while not tutorialName: tutorialName = dlg.pop()
dlg.destroy()
-
- # prepare tutorial for serialization
- self.tuto = Tutorial(tutorialName, self._tutorial)
- bundle = vault.TutorialBundler(self._guid)
- self._guid = bundle.Guid
- bundle.write_metadata_file(self.tuto)
- bundle.write_fsm(self._tutorial)
+ self._metadata = {
+ vault.INI_GUID_PROPERTY: self._guid,
+ vault.INI_NAME_PROPERTY: tutorialName,
+ vault.INI_VERSION_PROPERTY: '1',
+ 'activities':{os.environ['SUGAR_BUNDLE_NAME']:
+ os.environ['SUGAR_BUNDLE_VERSION']
+ },
+ }
+
+ vault.Vault.saveTutorial(self._tutorial, self._metadata)
def launch(*args, **kwargs):
diff --git a/tutorius/engine.py b/tutorius/engine.py
index e77a018..c945e49 100644
--- a/tutorius/engine.py
+++ b/tutorius/engine.py
@@ -4,17 +4,130 @@ from jarabe.model import shell
from sugar.bundle.activitybundle import ActivityBundle
from .vault import Vault
+from .TProbe import ProbeManager
+from .dbustools import save_args
+from .tutorial import Tutorial, AutomaticTransitionEvent
+
+
+class TutorialRunner(object):
+ """
+ Driver for the execution of one tutorial
+ """
+ def __init__(self, tutorial, probeManager):
+ """Constructor
+ @param tutorial Tutorial to execute
+ @param probeManager probeManager to use
+ """
+ self._tutorial = tutorial
+ self._pM = probeManager
+
+ #State
+ self._state = None
+ self._sEvents = set() #Subscribed Events
+
+ #Cached objects
+ self._actions = {}
+
+ #Temp FIX until event/actions have an activity id
+ self._activity_id = None
+
+ #Temp FIX until event, actions have an activity id
+ def setCurrentActivity(self):
+ self._pM.currentActivity = self._activity_id
+
+ def start(self):
+ self.setCurrentActivity() #Temp Hack until activity in events/actions
+ self.enterState(self._tutorial.INIT)
+
+ def stop(self):
+ self.setCurrentActivity() #Temp Hack until activity in events/actions
+ self.enterState(self._tutorial.END)
+ self._teardownState()
+ self._state = None
+
+ def _handleEvent(self, next_state, event):
+ #FIXME sanity check, log event that was not installed and ignore
+ self.enterState(next_state)
+
+ def _teardownState(self):
+ if self._state is None:
+ #No state, no teardown
+ return
+
+ #Clear the current actions
+ for action in self._actions.values():
+ self._pM.uninstall(action)
+ self._actions = {}
+
+ #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)
+ transitions = self._tutorial.get_transition_dict(self._state)
+
+ for (event, next_state) in transitions.values():
+ if isinstance(event, AutomaticTransitionEvent):
+ state_name = next_state
+ break
+
+ self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state)))
+
+ for action in self._actions.values():
+ self._pM.install(action)
+
+ return state_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.
+
+ When entering the state, 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
+
+ # Recursive base case
+ if state_name == self._state:
+ #Nothing to do
+ return
+
+ 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())
+
+
+
class Engine:
"""
Driver for the execution of tutorials
"""
- def __init__(self):
- # FIXME Probe management should be in the probe manager
+ def __init__(self, probeManager=None):
+ """Constructor
+ @param probeManager (optional) ProbeManager instance to use
+ """
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
#FIXME shell.get_model() will only be useful in the shell process
self._shell = shell.get_model()
+ self._probeManager = probeManager or ProbeManager()
self._tutorial = None
def launch(self, tutorialID):
@@ -22,25 +135,33 @@ class Engine:
@param tutorialID unique tutorial identifier used to retrieve it from the disk
"""
if self._tutorial:
- self._tutorial.detach()
- self._tutorial = None
+ self.stop()
+
+ self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), self._probeManager)
#Get the active activity from the shell
activity = self._shell.get_active_activity()
- self._tutorial = Vault.loadTutorial(tutorialID)
-
#TProbes automatically use the bundle id, available from the ActivityBundle
bundle = ActivityBundle(activity.get_bundle_path())
- self._tutorial.attach(bundle.get_bundle_id())
- def stop(self):
+ self._tutorial._activity_id = bundle.get_bundle_id() #HACK until we have activity id's in action/events
+
+ self._tutorial.start()
+
+ def stop(self, tutorialID=None):
""" Stop the current tutorial
"""
- self._tutorial.detach()
+ if tutorialID is None:
+ logging.warning(
+ "stop() without a tutorialID will become deprecated")
+ self._tutorial.stop()
self._tutorial = None
- def pause(self):
+ def pause(self, tutorialID=None):
""" Interrupt the current tutorial and save its state in the journal
"""
+ if tutorialID is None:
+ logging.warning( \
+ "pause() without a tutorialID will become deprecated")
raise NotImplementedError("Unable to store tutorial state")
diff --git a/tutorius/service.py b/tutorius/service.py
index eb246a1..11a94a5 100644
--- a/tutorius/service.py
+++ b/tutorius/service.py
@@ -2,6 +2,9 @@ import dbus
from .engine import Engine
from .dbustools import remote_call
+from .TProbe import ProbeManager
+import logging
+LOGGER = logging.getLogger("sugar.tutorius.service")
_DBUS_SERVICE = "org.tutorius.Service"
_DBUS_PATH = "/org/tutorius/Service"
@@ -19,11 +22,13 @@ class Service(dbus.service.Object):
self._engine = None
+ self._probeMgr = ProbeManager()
+
def start(self):
""" Start the service itself
"""
# For the moment there is nothing to do
- pass
+ LOGGER.debug("Service.start()")
@dbus.service.method(_DBUS_SERVICE_IFACE,
@@ -33,7 +38,7 @@ class Service(dbus.service.Object):
@param tutorialID unique tutorial identifier used to retrieve it from the disk
"""
if self._engine == None:
- self._engine = Engine()
+ self._engine = Engine(self._probeMgr)
self._engine.launch(tutorialID)
@dbus.service.method(_DBUS_SERVICE_IFACE,
@@ -50,6 +55,35 @@ class Service(dbus.service.Object):
"""
self._engine.pause()
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="ss", out_signature="")
+ def register_probe(self, process_name, unique_id):
+ """ Adds a probe to the known probes, to be used by a tutorial.
+
+ A generic name for a process (like an Activity) is passed
+ so that the execution of a tutorial will use that generic
+ name. However, a unique id is also passed to differentiate
+ between many instances of the same process.
+
+ @param process_name The generic name of a process
+ @param unique_id The unique identification associated to this
+ process
+ """
+ LOGGER.debug("Service.register_probe(%s,%s)", process_name, unique_id)
+ self._probeMgr.register_probe(process_name, unique_id)
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="s", out_signature="")
+ def unregister_probe(self, unique_id):
+ """ Remove a probe from the known probes.
+
+ @param process_name The generic name of a process
+ @param unique_id The unique identification associated to this
+ process
+ """
+ LOGGER.debug("Service.unregister_probe(%s)", unique_id)
+ self._probeMgr.unregister_probe(unique_id)
+
class ServiceProxy:
""" Proxy to connect to the Service object, abstracting the DBus interface"""
@@ -74,6 +108,33 @@ class ServiceProxy:
"""
remote_call(self._service.pause, (), block=False)
+ def register_probe(self, process_name, unique_id):
+ """ Adds a probe to the known probes, to be used by a tutorial.
+
+ A generic name for a process (like an Activity) is passed
+ so that the execution of a tutorial will use that generic
+ name. However, a unique id is also passed to differentiate
+ between many instances of the same process.
+
+ @param process_name The generic name of a process
+ @param unique_id The unique identification associated to this
+ process
+ """
+ remote_call(self._service.register_probe, (process_name,unique_id), block=False)
+
+ def unregister_probe(self, unique_id):
+ """ Remove a probe from the known probes.
+
+ @param process_name The generic name of a process
+ @param unique_id The unique identification associated to this
+ process
+ """
+ # We make it synchronous because otherwise on closing,
+ # activities kill the dbus session bus too fast for the
+ # asynchronous call to be completed
+ self._service.unregister_probe(unique_id)
+
+
if __name__ == "__main__":
import dbus.mainloop.glib
import gobject
diff --git a/tutorius/viewer.py b/tutorius/viewer.py
index 272558e..56428e1 100644
--- a/tutorius/viewer.py
+++ b/tutorius/viewer.py
@@ -18,9 +18,7 @@ This module renders a widget containing a graphical representation
of a tutorial and acts as a creator proxy as it has some editing
functionality.
"""
-import sys
-
-import gtk, gtk.gdk
+import gtk
import cairo
from math import pi as PI
PI2 = PI/2
@@ -30,7 +28,6 @@ import rsvg
from sugar.bundle import activitybundle
from sugar.tutorius import addon
from sugar.graphics import icon
-from sugar.tutorius.filters import EventFilter
from sugar.tutorius.actions import Action
import os
@@ -66,7 +63,7 @@ class Viewer(object):
self.alloc = None
self.click_pos = None
self.drag_pos = None
- self.selection = []
+ self.selection = set()
self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.win.set_size_request(400, 200)
@@ -81,7 +78,7 @@ class Viewer(object):
canvas = gtk.DrawingArea()
vbox.add_with_viewport(canvas)
canvas.set_app_paintable(True)
- canvas.connect_after("expose-event", self.on_viewer_expose, tutorial._states)
+ canvas.connect_after("expose-event", self.on_viewer_expose, tutorial)
canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK \
|gtk.gdk.BUTTON_MOTION_MASK \
|gtk.gdk.BUTTON_RELEASE_MASK \
@@ -99,10 +96,13 @@ class Viewer(object):
canvas.set_size_request(2048, 180) # FIXME
def destroy(self):
+ """
+ Destroy ui resources associated with this object.
+ """
self.win.destroy()
- def _paint_state(self, ctx, states):
+ def _paint_state(self, ctx, tutorial):
"""
Paints a tutorius fsm state in a cairo context.
Final context state will be shifted by the size of the graphics.
@@ -111,17 +111,13 @@ class Viewer(object):
block_max_height = self.alloc.height
new_insert_point = None
- cur_state = 'INIT'
+ state_name = tutorial.INIT
# FIXME: get app when we have a model that supports it
cur_app = 'Calculate'
app_start = ctx.get_matrix()
- try:
- state = states[cur_state]
- except KeyError:
- state = None
- while state:
+ while state_name:
new_app = 'Calculate'
if new_app != cur_app:
ctx.save()
@@ -132,16 +128,17 @@ class Viewer(object):
ctx.translate(BLOCK_PADDING, 0)
cur_app = new_app
- action_list = state.get_action_list()
+ action_list = tutorial.get_action_dict(state_name).items()
if action_list:
- local_height = (block_max_height - BLOCK_PADDING)/len(action_list) - BLOCK_PADDING
+ local_height = (block_max_height - BLOCK_PADDING)\
+ / len(action_list) - BLOCK_PADDING
ctx.save()
- for action in action_list:
+ for action_name, action in action_list:
origin = tuple(ctx.get_matrix())[-2:]
if self.click_pos and \
self.click_pos[0]-BLOCK_WIDTH<origin[0] and \
self.drag_pos[0]>origin[0]:
- self.selection.append(action)
+ self.selection.add((action_name, action))
self.render_action(ctx, block_width, local_height, action)
ctx.translate(0, local_height+BLOCK_PADDING)
@@ -150,7 +147,7 @@ class Viewer(object):
# insertion cursor painting made from two opposed triangles
# joined by a line.
- if state.name == self._creator.get_insertion_point():
+ if state_name == self._creator.get_insertion_point():
ctx.save()
bp2 = BLOCK_PADDING/2
ctx.move_to(-bp2, 0)
@@ -170,36 +167,34 @@ class Viewer(object):
ctx.restore()
- event_list = state.get_event_filter_list()
+ event_list = tutorial.get_transition_dict(state_name).items()
if event_list:
- local_height = (block_max_height - BLOCK_PADDING)/len(event_list) - BLOCK_PADDING
+ local_height = (block_max_height - BLOCK_PADDING)\
+ /len(event_list) - BLOCK_PADDING
ctx.save()
- for event, next_state in event_list:
+ for transition_name, transition in event_list:
origin = tuple(ctx.get_matrix())[-2:]
if self.click_pos and \
self.click_pos[0]-BLOCK_WIDTH<origin[0] and \
self.drag_pos[0]>origin[0]:
- self.selection.append(event)
- self.render_event(ctx, block_width, local_height, event)
+ self.selection.add((transition_name, transition))
+ self.render_event(ctx, block_width, local_height,
+ event=transition[0])
ctx.translate(0, local_height+BLOCK_PADDING)
ctx.restore()
ctx.translate(BLOCK_WIDTH, 0)
- # FIXME point to next state in state, as it would highlight
- # the "happy path".
- cur_state = event_list[0][1]
-
if (not new_insert_point) and self.click_pos:
origin = tuple(ctx.get_matrix())[-2:]
if self.click_pos[0]<origin[0]:
- new_insert_point = state
+ new_insert_point = state_name
- if event_list:
- try:
- state = states[cur_state]
- except KeyError:
- break
+ if event_list and state_name != tutorial.END:
+ # TODO: use marked path, to avoid infinite loops on recursive
+ # tutorials.
+ next_states = tutorial.get_following_states_dict(state_name)
+ state_name = next_states.keys()[0]
yield True
else:
break
@@ -209,9 +204,9 @@ class Viewer(object):
if self.click_pos:
if not new_insert_point:
- new_insert_point = state
+ new_insert_point = state_name
- self._creator.set_insertion_point(new_insert_point.name)
+ self._creator.set_insertion_point(new_insert_point)
yield False
@@ -235,7 +230,7 @@ class Viewer(object):
def _render_app_hints(self, ctx, appname):
"""
- Fetches the icon of the app related to current states and renders it on a
+ Fetches the icon of the app related to current state and renders it on a
separator, between states.
"""
ctx.set_source_rgb(0.0, 0.0, 0.0)
@@ -248,11 +243,11 @@ class Viewer(object):
bundle_path = os.getenv("SUGAR_BUNDLE_PATH")
if bundle_path:
icon_path = activitybundle.ActivityBundle(bundle_path).get_icon()
- icon = rsvg.Handle(icon_path)
+ icon_rsvg = rsvg.Handle(icon_path)
ctx.save()
ctx.translate(-15, 0)
ctx.scale(0.5, 0.5)
- icon_surf = icon.render_cairo(ctx)
+ icon_rsvg.render_cairo(ctx)
ctx.restore()
@@ -278,9 +273,12 @@ class Viewer(object):
ctx.move_to(BLOCK_CORNERS, 0)
paint_border(inner_width, 0)
- ctx.arc(inner_width+BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI2, 0.0)
- ctx.arc(inner_width+BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, 0.0, PI2)
- ctx.arc(BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, PI2, PI)
+ ctx.arc(inner_width+BLOCK_CORNERS, BLOCK_CORNERS,
+ BLOCK_CORNERS, -PI2, 0.0)
+ ctx.arc(inner_width+BLOCK_CORNERS, inner_height+BLOCK_CORNERS,
+ BLOCK_CORNERS, 0.0, PI2)
+ ctx.arc(BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS,
+ PI2, PI)
ctx.arc(BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI, -PI2)
ctx.set_source(tracing)
@@ -295,7 +293,7 @@ class Viewer(object):
ctx.save()
ctx.translate(BLOCK_INNER_PAD, BLOCK_INNER_PAD)
ctx.scale(0.5, 0.5)
- icon_surf = rsvg_icon.render_cairo(ctx)
+ rsvg_icon.render_cairo(ctx)
ctx.restore()
@@ -340,29 +338,30 @@ class Viewer(object):
addon_name = addon.get_name_from_type(type(event))
# TODO use icon pool
- icon_name = addon.get_addon_meta(addon_name)['icon']
- rsvg_icon = rsvg.Handle(icon.get_icon_file_name(icon_name))
- ctx.save()
- ctx.translate(BLOCK_INNER_PAD, BLOCK_INNER_PAD)
- ctx.scale(0.5, 0.5)
- icon_surf = rsvg_icon.render_cairo(ctx)
+ if addon_name:
+ icon_name = addon.get_addon_meta(addon_name)['icon']
+ rsvg_icon = rsvg.Handle(icon.get_icon_file_name(icon_name))
+ ctx.save()
+ ctx.translate(BLOCK_INNER_PAD, BLOCK_INNER_PAD)
+ ctx.scale(0.5, 0.5)
+ rsvg_icon.render_cairo(ctx)
- ctx.restore()
+ ctx.restore()
ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2)
self._render_snapshot(ctx, event)
ctx.restore()
- def on_viewer_expose(self, widget, evt, states):
+ def on_viewer_expose(self, widget, evt, tutorial):
"""
Expose signal handler for the viewer's DrawingArea.
- This loops through states and renders every action and transition of
+ This loops through tutorial and renders every action and transition of
the "happy path".
@param widget: the gtk.DrawingArea on which to draw
@param evt: the gtk.gdk.Event containing an "expose" event
- @param states: a tutorius FiniteStateMachine object to paint
+ @param tutorial: a tutorius FiniteStateMachine object to paint
"""
ctx = widget.window.cairo_create()
self.alloc = widget.get_allocation()
@@ -378,9 +377,12 @@ class Viewer(object):
ctx.clip()
ctx.paint()
+ # padding internal to the widget, to draw the first half of the
+ # activity app hint (the icon)
+ ctx.translate(20, 0)
ctx.translate(BLOCK_PADDING, BLOCK_PADDING)
- painter = self._paint_state(ctx, states)
+ painter = self._paint_state(ctx, tutorial)
while painter.next(): pass
if self.click_pos and self.drag_pos:
@@ -400,7 +402,7 @@ class Viewer(object):
self.drag_pos = self.click_pos = evt.get_coords()
widget.queue_draw()
- self.selection = []
+ self.selection.clear()
def _on_drag(self, widget, evt):
self.drag_pos = evt.get_coords()
@@ -413,11 +415,12 @@ class Viewer(object):
def _on_key_press(self, widget, evt):
if evt.keyval == gtk.keysyms.BackSpace:
# remove selection
- for selected in self.selection:
- if isinstance(selected, EventFilter):
- self._creator.delete_state()
+ for name, obj in self.selection:
+ if isinstance(obj, Action):
+ self._creator.delete_action(name)
else:
- self._creator.delete_action(selected)
+ self._creator.delete_state()
+ self.selection.clear()
widget.queue_draw()