Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--addons/bubblemessage.py8
-rw-r--r--addons/chainaction.py2
-rw-r--r--addons/clickaction.py4
-rw-r--r--addons/dialogmessage.py2
-rw-r--r--addons/disablewidget.py6
-rw-r--r--addons/gtkwidgeteventfilter.py10
-rw-r--r--addons/gtkwidgettypefilter.py8
-rw-r--r--addons/oncewrapper.py2
-rw-r--r--addons/readfile.py6
-rw-r--r--addons/timerevent.py4
-rw-r--r--addons/triggereventfilter.py4
-rw-r--r--addons/typetextaction.py4
-rw-r--r--addons/widgetidentifyaction.py4
-rw-r--r--data/icons/clock.svg4
-rw-r--r--data/ui/creator.glade209
-rwxr-xr-xsetup.py4
-rw-r--r--tests/coretests.py17
-rw-r--r--tests/filterstests.py20
-rw-r--r--tests/probetests.py483
-rw-r--r--tutorius/TProbe.py30
-rw-r--r--tutorius/actions.py7
-rw-r--r--tutorius/addon.py14
-rw-r--r--tutorius/core.py13
-rw-r--r--tutorius/creator.py646
-rw-r--r--tutorius/editor.py2
-rw-r--r--tutorius/engine.py4
-rw-r--r--tutorius/filters.py2
-rw-r--r--tutorius/linear_creator.py8
-rw-r--r--tutorius/overlayer.py4
-rw-r--r--tutorius/properties.py16
-rw-r--r--tutorius/service.py4
-rw-r--r--tutorius/vault.py6
-rw-r--r--tutorius/viewer.py423
33 files changed, 1684 insertions, 296 deletions
diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py
index 2bd2d31..6572a6a 100644
--- a/addons/bubblemessage.py
+++ b/addons/bubblemessage.py
@@ -13,10 +13,10 @@
# 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, TArrayProperty
-from sugar.tutorius import overlayer
-from sugar.tutorius.services import ObjectStore
+from ..actions import Action, DragWrapper
+from ..properties import TStringProperty, TArrayProperty
+from .. import overlayer
+from ..services import ObjectStore
class BubbleMessage(Action):
message = TStringProperty("Message")
diff --git a/addons/chainaction.py b/addons/chainaction.py
index 43c4fa4..8df7ac8 100644
--- a/addons/chainaction.py
+++ b/addons/chainaction.py
@@ -14,7 +14,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from sugar.tutorius.actions import *
+from ..actions import *
class ChainAction(Action):
actions = TAddonListProperty()
diff --git a/addons/clickaction.py b/addons/clickaction.py
index 828dd75..88c5519 100644
--- a/addons/clickaction.py
+++ b/addons/clickaction.py
@@ -14,8 +14,8 @@
# 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 import gtkutils
-from sugar.tutorius.actions import *
+from .. import gtkutils
+from ..actions import *
class ClickAction(Action):
"""
diff --git a/addons/dialogmessage.py b/addons/dialogmessage.py
index f15f256..9250693 100644
--- a/addons/dialogmessage.py
+++ b/addons/dialogmessage.py
@@ -16,7 +16,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from sugar.tutorius.actions import *
+from ..actions import *
class DialogMessage(Action):
message = TStringProperty("Message")
diff --git a/addons/disablewidget.py b/addons/disablewidget.py
index ce3f235..fd88303 100644
--- a/addons/disablewidget.py
+++ b/addons/disablewidget.py
@@ -14,9 +14,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from sugar.tutorius.actions import *
-from sugar.tutorius import gtkutils
-from sugar.tutorius.services import ObjectStore
+from ..actions import *
+from .. import gtkutils
+from ..services import ObjectStore
class DisableWidgetAction(Action):
target = TStringProperty("0")
diff --git a/addons/gtkwidgeteventfilter.py b/addons/gtkwidgeteventfilter.py
index 5497af4..65aa744 100644
--- a/addons/gtkwidgeteventfilter.py
+++ b/addons/gtkwidgeteventfilter.py
@@ -13,16 +13,16 @@
# 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.filters import EventFilter
-from sugar.tutorius.properties import TUAMProperty, TStringProperty
-from sugar.tutorius.gtkutils import find_widget
+from ..filters import EventFilter
+from ..properties import TUAMProperty, TEventType
+from ..gtkutils import find_widget
class GtkWidgetEventFilter(EventFilter):
"""
Basic Event filter for Gtk widget events
"""
object_id = TUAMProperty()
- event_name = TStringProperty("clicked")
+ event_name = TEventType('clicked')
def __init__(self, object_id=None, event_name=None):
"""Constructor
@@ -64,6 +64,6 @@ __event__ = {
"display_name" : "GTK Event catcher",
"icon" : "player_play",
"class" : GtkWidgetEventFilter,
- "mandatory_props" : ["object_id"]
+ "mandatory_props" : ["object_id", "event_name"]
}
diff --git a/addons/gtkwidgettypefilter.py b/addons/gtkwidgettypefilter.py
index 816a754..4ffecb5 100644
--- a/addons/gtkwidgettypefilter.py
+++ b/addons/gtkwidgettypefilter.py
@@ -14,10 +14,10 @@
# 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.filters import *
-from sugar.tutorius.properties import *
-from sugar.tutorius.services import ObjectStore
-from sugar.tutorius.gtkutils import find_widget
+from ..filters import *
+from ..properties import *
+from ..services import ObjectStore
+from ..gtkutils import find_widget
import logging
logger = logging.getLogger("GtkWidgetTypeFilter")
diff --git a/addons/oncewrapper.py b/addons/oncewrapper.py
index 97f4752..5db3b60 100644
--- a/addons/oncewrapper.py
+++ b/addons/oncewrapper.py
@@ -14,7 +14,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from sugar.tutorius.actions import *
+from ..actions import *
class OnceWrapper(Action):
"""
diff --git a/addons/readfile.py b/addons/readfile.py
index 0d276b9..9fe2f81 100644
--- a/addons/readfile.py
+++ b/addons/readfile.py
@@ -16,9 +16,9 @@
import os
-from sugar.tutorius.actions import Action
-from sugar.tutorius.properties import TFileProperty
-from sugar.tutorius.services import ObjectStore
+from ..actions import Action
+from ..properties import TFileProperty
+from ..services import ObjectStore
class ReadFile(Action):
filename = TFileProperty(None)
diff --git a/addons/timerevent.py b/addons/timerevent.py
index c7374d0..752a865 100644
--- a/addons/timerevent.py
+++ b/addons/timerevent.py
@@ -16,8 +16,8 @@
import gobject
-from sugar.tutorius.filters import EventFilter
-from sugar.tutorius.properties import TIntProperty
+from ..filters import EventFilter
+from ..properties import TIntProperty
class TimerEvent(EventFilter):
"""
diff --git a/addons/triggereventfilter.py b/addons/triggereventfilter.py
index 6a0c2c9..19544b0 100644
--- a/addons/triggereventfilter.py
+++ b/addons/triggereventfilter.py
@@ -14,8 +14,8 @@
# 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.filters import *
-from sugar.tutorius.properties import *
+from ..filters import *
+from ..properties import *
class TriggerEventFilter(EventFilter):
"""
diff --git a/addons/typetextaction.py b/addons/typetextaction.py
index fee66e5..8b746e6 100644
--- a/addons/typetextaction.py
+++ b/addons/typetextaction.py
@@ -14,8 +14,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from sugar.tutorius.actions import *
-from sugar.tutorius import gtkutils
+from ..actions import *
+from .. import gtkutils
class TypeTextAction(Action):
"""
diff --git a/addons/widgetidentifyaction.py b/addons/widgetidentifyaction.py
index 3c66211..3df244b 100644
--- a/addons/widgetidentifyaction.py
+++ b/addons/widgetidentifyaction.py
@@ -14,9 +14,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from sugar.tutorius.actions import *
+from ..actions import *
-from sugar.tutorius.editor import WidgetIdentifier
+from ..editor import WidgetIdentifier
class WidgetIdentifyAction(Action):
def __init__(self):
diff --git a/data/icons/clock.svg b/data/icons/clock.svg
index 8adb898..dc73bbb 100644
--- a/data/icons/clock.svg
+++ b/data/icons/clock.svg
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="231" height="231" id="svg2" sodipodi:version="0.32" inkscape:version="0.44+devel" sodipodi:docbase="C:\Documents and Settings\Molumen\Desktop" sodipodi:docname="clock_beige.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" sodipodi:modified="true" version="1.0">
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="51" height="51" id="svg2" sodipodi:version="0.32" inkscape:version="0.44+devel" sodipodi:docbase="C:\Documents and Settings\Molumen\Desktop" sodipodi:docname="clock_beige.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" sodipodi:modified="true" version="1.0">
<defs id="defs4">
<linearGradient y2="84.524567" x2="302" y1="365.95651" x1="302" gradientUnits="userSpaceOnUse" id="linearGradient20470" xlink:href="#linearGradient13034" inkscape:collect="always"/>
<radialGradient r="90.78125" fy="691.20294" fx="527" cy="691.20294" cx="527" gradientTransform="matrix(1, 0, 0, 0.231842, -340, 200.219)" gradientUnits="userSpaceOnUse" id="radialGradient20468" xlink:href="#linearGradient12977" inkscape:collect="always"/>
@@ -266,4 +266,4 @@
<path sodipodi:ry="138" sodipodi:rx="138" transform="matrix(0.728261, 0, 0, 0.601449, 306.065, 286.927)" d="M 440,288.36218 A 138,138 0 1 1 164,288.36218 A 138,138 0 1 1 440,288.36218 z" sodipodi:type="arc" sodipodi:cy="288.36218" sodipodi:cx="302" id="path19429" style="fill: url(#linearGradient19441) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
</g>
</g>
-</svg> \ No newline at end of file
+</svg>
diff --git a/data/ui/creator.glade b/data/ui/creator.glade
new file mode 100644
index 0000000..1c9669d
--- /dev/null
+++ b/data/ui/creator.glade
@@ -0,0 +1,209 @@
+<?xml version="1.0"?>
+<glade-interface>
+ <!-- interface-requires gtk+ 2.16 -->
+ <!-- interface-naming-policy project-wide -->
+ <widget class="GtkWindow" id="mainwindow">
+ <property name="width_request">300</property>
+ <property name="height_request">500</property>
+ <property name="title" translatable="yes">Toolbox</property>
+ <property name="resizable">False</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="default_width">200</property>
+ <property name="default_height">500</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">True</property>
+ <property name="focus_on_map">False</property>
+ <property name="deletable">False</property>
+ <signal name="destroy" handler="on_mainwindow_destroy"/>
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">5</property>
+ <child>
+ <widget class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="spacing">5</property>
+ <property name="layout_style">start</property>
+ <child>
+ <widget class="GtkButton" id="button2">
+ <property name="label">gtk-save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_save_clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="button4">
+ <property name="label">gtk-quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_quit_clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <widget class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="resize_mode">queue</property>
+ <child>
+ <widget class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkExpander" id="expander1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <widget class="GtkIconView" id="iconview1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="columns">2</property>
+ <property name="row_spacing">0</property>
+ <property name="column_spacing">0</property>
+ <property name="item_padding">0</property>
+ <signal name="item_activated" handler="on_action_activate"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">actions</property>
+ </widget>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkExpander" id="expander2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <widget class="GtkIconView" id="iconview2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="columns">2</property>
+ <property name="row_spacing">0</property>
+ <property name="column_spacing">0</property>
+ <property name="item_padding">0</property>
+ <signal name="item_activated" handler="on_event_activate"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">events</property>
+ </widget>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="propbox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">10</property>
+ <child>
+ <placeholder/>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHButtonBox" id="hbuttonbox2">
+ <property name="visible">True</property>
+ <property name="spacing">5</property>
+ <property name="layout_style">start</property>
+ <child>
+ <widget class="GtkButton" id="button1">
+ <property name="label">gtk-media-record</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="button3">
+ <property name="label">gtk-media-stop</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+</glade-interface>
diff --git a/setup.py b/setup.py
index 9362dc7..a994937 100755
--- a/setup.py
+++ b/setup.py
@@ -99,7 +99,9 @@ setup(name='Tutorius',
'sugar.tutorius.addons': 'addons',
},
cmdclass = {'test': TestCommand},
- data_files=[('share/icons/sugar/scalable/actions', glob.glob('data/icons/*.svg')),]
+ data_files=[('share/icons/sugar/scalable/actions', glob.glob('data/icons/*.svg')),
+ ('share/tutorius/ui', glob.glob('data/ui/*.glade')),
+ ]
)
# vim: set et sw=4 sts=4 ts=4:
diff --git a/tests/coretests.py b/tests/coretests.py
index 4d5055e..b9e04e5 100644
--- a/tests/coretests.py
+++ b/tests/coretests.py
@@ -28,7 +28,7 @@ and event filters. Those are in their separate test module
import unittest
-import copy
+from copy import deepcopy
import logging
from sugar.tutorius.actions import *
from sugar.tutorius.addon import *
@@ -275,22 +275,21 @@ class StateTest(unittest.TestCase):
assert not(st1 == st2), "Different state names should give different states"
st2.name = "Identical"
- st3 = copy.deepcopy(st1)
+ st3 = deepcopy(st1)
st3.add_action(addon.create("BubbleMessage", "Hi!", [128,264]))
assert not (st1 == st3), "States having a different number of actions should be different"
- st4 = copy.deepcopy(st1)
- st4.add_event_filter(addon.create("GtkWidgetEventFilter", "next_state", "0.0.1.1.2.2.3", "clicked"))
+ st4 = deepcopy(st1)
+ st4.add_event_filter(addon.create("GtkWidgetEventFilter", "0.0.1.1.2.2.3", "clicked"), "next_state")
assert not (st1 == st4), "States having a different number of events should be different"
- st5 = copy.deepcopy(st1)
+ st5 = deepcopy(st1)
st5._event_filters = []
- st5.add_event_filter(addon.create("GtkWidgetEventFilter", "other_state", "0.1.2.3.4.1.2", "pressed"))
+ st5.add_event_filter(addon.create("GtkWidgetEventFilter", "0.1.2.3.4.1.2", "pressed"), "other_state")
- #import rpdb2; rpdb2.start_embedded_debugger('pass')
assert not (st1 == st5), "States having the same number of event filters" \
+ " but those being different should be different"
@@ -523,7 +522,7 @@ class FSMTest(unittest.TestCase):
fsm.add_action(act1)
- fsm2 = copy.deepcopy(fsm)
+ fsm2 = deepcopy(fsm)
assert fsm == fsm2
@@ -548,7 +547,7 @@ class FSMTest(unittest.TestCase):
fsm.add_state(st1)
fsm.add_state(st2)
- fsm4 = copy.deepcopy(fsm)
+ fsm4 = deepcopy(fsm)
assert fsm == fsm4
diff --git a/tests/filterstests.py b/tests/filterstests.py
index c45f924..ee6033b 100644
--- a/tests/filterstests.py
+++ b/tests/filterstests.py
@@ -32,20 +32,10 @@ from gtkutilstests import SignalCatcher
class BaseEventFilterTests(unittest.TestCase):
"""Test the behavior of the Base EventFilter class"""
- def test_properties(self):
- """Test EventFilter properties"""
- e = EventFilter("NEXTSTATE")
-
- assert e.next_state == "NEXTSTATE", "next_state should have value used in constructor"
-
- e.next_state = "NEWSTATE"
-
- assert e.next_state == "NEWSTATE", "next_state should have been changed by setter"
-
def test_callback(self):
"""Test the callback mechanism"""
- e = EventFilter("Next")
+ e = EventFilter()
s = SignalCatcher()
#Trigger the do_callback, shouldn't do anything
@@ -79,7 +69,7 @@ class TestTimerEvent(unittest.TestCase):
ctx = gobject.MainContext()
main = gobject.MainLoop(ctx)
- e = addon.create('TimerEvent', "Next", 2) # 2 seconds should be enough :s
+ e = addon.create('TimerEvent', 2) # 2 seconds should be enough :s
s = SignalCatcher()
e.install_handlers(s.callback)
@@ -122,7 +112,7 @@ class TestTimerEvent(unittest.TestCase):
ctx = gobject.MainContext()
main = gobject.MainLoop(ctx)
- e = addon.create('TimerEvent', "Next", 2) # 2 seconds should be enough :s
+ e = addon.create('TimerEvent', 2) # 2 seconds should be enough :s
s = SignalCatcher()
e.install_handlers(s.callback)
@@ -169,7 +159,7 @@ class TestGtkWidgetEventFilter(unittest.TestCase):
self.top.add(self.btn1)
def test_install(self):
- h = addon.create('GtkWidgetEventFilter', "Next","0","whatever")
+ h = addon.create('GtkWidgetEventFilter', "0","whatever")
try:
h.install_handlers(None)
@@ -178,7 +168,7 @@ class TestGtkWidgetEventFilter(unittest.TestCase):
assert True, "Install should have failed"
def test_button_clicks(self):
- h = addon.create('GtkWidgetEventFilter', "Next","0.0","clicked")
+ h = addon.create('GtkWidgetEventFilter', "0.0","clicked")
s = SignalCatcher()
h.install_handlers(s.callback, activity=self.top)
diff --git a/tests/probetests.py b/tests/probetests.py
index a440334..e1a587b 100644
--- a/tests/probetests.py
+++ b/tests/probetests.py
@@ -19,45 +19,482 @@ Probe Tests
"""
import unittest
-import os, sys
-import gtk
-import time
+import pickle
from dbus.mainloop.glib import DBusGMainLoop
+from dbus.mainloop import NULL_MAIN_LOOP
import dbus
-from sugar.tutorius.TProbe import TProbe, ProbeProxy
+from sugar.tutorius.TProbe import TProbe, ProbeProxy, ProbeManager
+from sugar.tutorius import addon
+from sugar.tutorius.actions import Action
+from sugar.tutorius.properties import TIntProperty, TStringProperty
-class FakeActivity(object):
- def __init__(self):
- self.top = gtk.Window(type=gtk.WINDOW_TOPLEVEL)
- self.top.set_name("Top")
+#Create a substitute addon create function
+old_addon_create = addon.create
+fake_addon_cache = {}
+def new_addon_create(name, *args, **kwargs):
+ if name in fake_addon_cache:
+ return fake_addon_cache[name](*args, **kwargs)
+ else:
+ return old_addon_create(name, *args, **kwargs)
+
+message_box = None
+event_box = None
+
+class MockAddon(Action):
+ i = TIntProperty(0)
+ s = TStringProperty("test")
+
+ def do(self):
+ global message_box
+ message_box = (self.i, self.s)
+
+ def undo(self):
+ global message_box
+ message_box = None
+
+ def install_handlers(self, callback, **kwargs):
+ global message_box
+ message_box = callback
+
+ def remove_handlers(self):
+ global message_box
+ message_box = None
+
+fake_addon_cache["MockAddon"] = MockAddon
+
+class MockActivity(object):
+ pass
+
+class MockProbeProxy(object):
+ _MockProxyCache = {}
+ def __new__(cls, activityName):
+ #For testing, use only one instance per activityName
+ return cls._MockProxyCache.setdefault(activityName, super(MockProbeProxy, cls).__new__(cls))
+
+ def __init__(self, activityName):
+ """
+ Constructor
+ @param activityName unique activity id. Must be a valid dbus bus name.
+ """
+ self.MockAction = None
+ self.MockActionUpdate = None
+ self.MockEvent = None
+ self.MockCB = None
+ self.MockAlive = True
+ self.MockEventAddr = None
- hbox = gtk.HBox()
- self.top.add(hbox)
- hbox.show()
+ def isAlive(self):
+ return self.MockAlive
+
+ def install(self, action, block=False):
+ self.MockAction = action
+ self.MockActionUpdate = None
+ return None
+
+ def update(self, action, newaction, block=False):
+ self.MockAction = action
+ self.MockActionUpdate = newaction
+ return None
- btn1 = gtk.Button()
- btn1.set_name("Button1")
- hbox.pack_start(btn1)
- btn1.show()
- self.button = btn1
+ def uninstall(self, action, block=False):
+ self.MockAction = None
+ self.MockActionUpdate = None
+ return None
+ def subscribe(self, event, callback, block=True):
+ #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
+ return str(id(event))
+
+ def unsubscribe(self, address, block=True):
+ self.MockEventAddr = address
+ return None
+
+ def detach(self, block=False):
+ self.MockAction = None
+ self.MockActionUpdate = None
+ self.MockEvent = None
+ self.MockCB = None
+ self.MockAlive = False
+ self.MockEventAddr = None
+ return None
+
+class MockProxyObject(object):
+ _MockProxyObjects = {}
+ def __new__(cls, name, path):
+ return cls._MockProxyObjects.setdefault((name, path), super(MockProxyObject, cls).__new__(cls))
+
+ def __init__(self, name, path):
+ self.MockCall = {}
+ self.MockRet = {}
+ self.MockCB = {}
+
+ def get_dbus_method(self, name, *args, **kwargs):
+ #FIXME This mockMethod should support asynchronous calling,
+ # and possibly more
+ def mockMethod(*a, **kw):
+ self.MockCall[name] = dict(args=a, kwargs=kw)
+ return self.MockRet.get(name, None)
+ return mockMethod
+
+ def connect_to_signal(self, signal_name, handler_function, dbus_interface=None, **kw):
+ self.MockCB[signal_name] = dict(handler_function=handler_function, dbus_interface=dbus_interface, **kw)
+
+class MockSessionBus(object):
+ def get_object(self, bus_name, object_path, introspect=True, follow_name_owner_changes=False, **kwargs):
+ return MockProxyObject(bus_name, object_path)
+
+old_SessionBus = dbus.SessionBus
+
+###########################################################################
+# Begin Test Cases
+###########################################################################
class ProbeTest(unittest.TestCase):
- def test_ping(self):
+ def setUp(self):
+ global message_box
+ message_box = None
+
+ #Fix the addon create
+ addon.create = new_addon_create
+
+ #Set a default dbus mainloop
m = DBusGMainLoop(set_as_default=True)
dbus.set_default_main_loop(m)
- activity = FakeActivity()
- probe = TProbe("localhost.unittest.ProbeTest", activity.top)
+ #Setup the activity and probe
+ self.activity = MockActivity()
+ self.probe = TProbe("localhost.unittest.ProbeTest", self.activity)
- #Parent, ping the probe
- proxy = ProbeProxy("localhost.unittest.ProbeTest")
- res = probe.ping()
-
+ #Override the eventOccured on the Probe...
+ self.old_eO = self.probe.eventOccured
+ def newEo(event):
+ global event_box
+ try:
+ self.old_eO(event)
+ event_box = event
+ except RuntimeError:
+ event_box = None
+
+ self.probe.eventOccured = newEo
+
+ def tearDown(self):
+ #Replace addon create
+ addon.create = old_addon_create
+
+ #Clear the default dbus mainloop
+ dbus.set_default_main_loop(NULL_MAIN_LOOP)
+
+ #Clear the activity
+ self.probe.remove_from_connection()
+ del self.probe
+ del self.activity
+
+ def test_ping(self):
+ #Test ping()
+ res = self.probe.ping()
assert res == "alive", "Probe should be alive"
+ def test_action(self):
+ global message_box
+ action = MockAddon()
+ action.i, action.s = (5,"woot")
+
+ assert message_box is None, "Message box should still be empty"
+
+ #install 1
+ address = self.probe.install(pickle.dumps(action))
+ assert type(address) == str, "install should return a string"
+ assert message_box == (5, "woot"), "message box should have (i, s)"
+
+ #install 2
+ action.i, action.s = (10, "ahhah!")
+ address2 = self.probe.install(pickle.dumps(action))
+ assert message_box == (10, "ahhah!"), "message box should have changed"
+ assert address != address2, "action addresses should be different"
+
+ #uninstall 2
+ self.probe.uninstall(address2)
+ assert message_box is None, "undo should clear the message box"
+
+ #update action 1 with action 2 props
+ self.probe.update(address, pickle.dumps(action._props))
+ assert message_box == (10, "ahhah!"), "message box should have changed(i, s)"
+
+ #ErrorCase: Update with bad address
+ #try to update 2, should fail
+ self.assertRaises(KeyError, self.probe.update, address2, pickle.dumps(action._props))
+
+ self.probe.uninstall(address)
+ assert message_box is None, "undo should clear the message box"
+
+ message_box = "Test"
+ #ErrorCase: Uninstall bad address (currently silent fail)
+ #Uninstall twice should do nothing
+ self.probe.uninstall(address)
+ assert message_box == "Test", "undo should not have happened again"
+
+ def test_events(self):
+ global message_box
+ global event_box
+
+ event = MockAddon()
+ event.i, event.s = (0, "event1")
+ event2 = MockAddon()
+ event2.i, event2.s = (1, "event2")
+
+ addr = self.probe.subscribe(pickle.dumps(event))
+ cb1 = message_box
+ addr2 = self.probe.subscribe(pickle.dumps(event2))
+ cb2 = message_box
+ assert type(addr) == str, "should return a string address"
+ assert addr != addr2, "each subscribe should return a different address"
+
+ assert event_box is None, "event_box should still be empty"
+ #Do the callback 2
+ cb2()
+
+ assert event_box is not None, "event_box should have an event"
+
+ assert type(event_box) == str, "event should be pickled"
+ assert pickle.loads(event_box) == event2, "event should be event2"
+
+ #Unsubscribe event 2
+ self.probe.unsubscribe(addr2)
+ assert message_box is None, "unsubscribe should clear the message_box"
+
+ #Do the callback 1
+ cb1()
+ assert pickle.loads(event_box) == event, "event should be event1"
+
+ #unsubscribe event 1
+ self.probe.unsubscribe(addr)
+ assert message_box is None, "unsubscribe should clear the message_box"
+
+ event_box = None
+ #ErrorCase: callback called from unregistered event filter
+ #Do the callback 1 again
+ self.assertRaises(RuntimeWarning, cb1)
+
+class ProbeManagerTest(unittest.TestCase):
+ def setUp(self):
+ 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"
+
+ #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"
+
+ def test_actions(self):
+ self.probeManager.attach("act1")
+ self.probeManager.attach("act2")
+ act1 = MockProbeProxy("act1")
+ act2 = MockProbeProxy("act2")
+
+ ad1 = MockAddon()
+ #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)
+ 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)
+ 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)
+ 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.currentActivity = "act1"
+ self.probeManager.uninstall(ad1)
+ 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")
+
+ ad1 = MockAddon()
+ ad2 = MockAddon()
+ ad2.i, ad2.s = (2, "test2")
+
+ 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.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)
+ 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"
+
+ self.probeManager.unsubscribe("SomeAddress")
+ 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")
+
+ def tearDown(self):
+ dbus.SessionBus = old_SessionBus
+ MockProxyObject._MockProxyObjects = {}
+
+ def test_Alive(self):
+ self.mockObj.MockRet["ping"] = "alive"
+ assert self.probeProxy.isAlive() == True, "Alive should return True"
+
+ self.mockObj.MockRet["ping"] = "anything else"
+ assert self.probeProxy.isAlive() == False, "Alive should return False"
+
+ def test_actions(self):
+ action = MockAddon()
+ action.i, action.s = 5, "action"
+ action2 = MockAddon()
+ action2.i, action2.s = 10, "action2"
+
+ #Check if the installed action is the good one
+ address = "Addr1"
+ #Set the return value of probe install
+ self.mockObj.MockRet["install"] = address
+ self.probeProxy.install(action, block=True)
+ assert pickle.loads(self.mockObj.MockCall["install"]["args"][0]) == action, "1 argument, the action"
+
+ #ErrorCase: Update should fail on noninstalled actions
+ self.assertRaises(RuntimeWarning, self.probeProxy.update, action2, action2, block=True)
+
+ #Test the update
+ self.probeProxy.update(action, action2, block=True)
+ 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)
+ assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed"
+
+ self.probeProxy.uninstall(action, block=True)
+ 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"
+ event2 = MockAddon()
+ event2.i, event2.s = 10, "event2"
+
+ def callback(event):
+ global message_box
+ message_box = event
+
+ #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)
+ assert pickle.loads(self.mockObj.MockCall["subscribe"]["args"][0]) == event, "1 argument, the event"
+
+ #Call the callback with the event
+ global message_box
+ self.mockObj.MockCB["eventOccured"]["handler_function"](pickle.dumps(event))
+ assert message_box == event, "callback should have been called with event"
+ message_box = None
+
+ #ErrorCase: eventOccured triggered by a wrong event
+ #Call with a wrong event
+ self.mockObj.MockCB["eventOccured"]["handler_function"](pickle.dumps(event2))
+ assert message_box is None, "callback should not have been called"
+
+
+ #ErrorCase: unsubcribe for non subscribed event
+ #Test the unsubscribe
+ self.probeProxy.unsubscribe("otheraddress", block=True)
+ 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"
+
+ #ErrorCase: eventOccured triggered by uninstalled event
+ #Test the callback with unregistered event
+ self.mockObj.MockCB["eventOccured"]["handler_function"](pickle.dumps(event))
+ assert message_box is None, "callback should not have been called"
+
if __name__ == "__main__":
unittest.main()
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
index e18ed67..f55547c 100644
--- a/tutorius/TProbe.py
+++ b/tutorius/TProbe.py
@@ -8,12 +8,12 @@ import dbus
import dbus.service
import cPickle as pickle
-import sugar.tutorius.addon as addon
-from sugar.tutorius.services import ObjectStore
-from sugar.tutorius.properties import TPropContainer
+from . import addon
+from .services import ObjectStore
+from .properties import TPropContainer
-from sugar.tutorius.dbustools import remote_call, save_args
+from .dbustools import remote_call, save_args
import copy
"""
@@ -195,7 +195,11 @@ class TProbe(dbus.service.Object):
# The actual method we will call on the probe to send events
def notify(self, event):
LOGGER.debug("TProbe :: notify event %s", str(event))
- self.eventOccured(pickle.dumps(event))
+ #Check that this event is even allowed
+ if event in self._subscribedEvents.values():
+ self.eventOccured(pickle.dumps(event))
+ else:
+ raise RuntimeWarning("Attempted to raise an unregistered event")
# Return a unique name for this action
def _generate_action_reference(self, action):
@@ -400,8 +404,8 @@ class ProbeProxy:
Detach the ProbeProxy from it's TProbe. All installed actions and
subscribed events should be removed.
"""
- for action in self._actions.keys():
- self.uninstall(action, block)
+ for action_addr in self._actions.keys():
+ self.uninstall(action_addr, block)
for address in self._subscribedEvents.keys():
self.unsubscribe(address, block)
@@ -414,7 +418,13 @@ 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
"""
- def __init__(self):
+ 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
+ """
+ self._ProxyClass = proxy_class
self._probes = {}
self._current_activity = None
@@ -431,7 +441,7 @@ class ProbeManager(object):
if activity_id in self._probes:
raise RuntimeWarning("Activity already attached")
- self._probes[activity_id] = ProbeProxy(activity_id)
+ 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!"
@@ -442,6 +452,8 @@ class ProbeManager(object):
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):
"""
diff --git a/tutorius/actions.py b/tutorius/actions.py
index 08f55cd..bb15459 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -20,11 +20,12 @@ import gtk
from gettext import gettext as _
-from sugar.tutorius import addon
-from sugar.tutorius.services import ObjectStore
-from sugar.tutorius.properties import *
from sugar.graphics import icon
+from . import addon
+from .services import ObjectStore
+from .properties import *
+
class DragWrapper(object):
"""Wrapper to allow gtk widgets to be dragged around"""
def __init__(self, widget, position, draggable=False):
diff --git a/tutorius/addon.py b/tutorius/addon.py
index 15612c8..7ac68f7 100644
--- a/tutorius/addon.py
+++ b/tutorius/addon.py
@@ -38,6 +38,9 @@ import logging
PREFIX = __name__+"s"
PATH = re.sub("addon\\.py[c]$", "", __file__)+"addons"
+TYPE_ACTION = 'action'
+TYPE_EVENT = 'event'
+
_cache = None
def _reload_addons():
@@ -47,9 +50,11 @@ def _reload_addons():
mod = __import__(PREFIX+'.'+re.sub("\\.py$", "", addon), {}, {}, [""])
if hasattr(mod, "__action__"):
_cache[mod.__action__['name']] = mod.__action__
+ mod.__action__['type'] = TYPE_ACTION
continue
if hasattr(mod, "__event__"):
_cache[mod.__event__['name']] = mod.__event__
+ mod.__event__['type'] = TYPE_EVENT
def create(name, *args, **kwargs):
global _cache
@@ -78,4 +83,13 @@ def get_addon_meta(name):
_reload_addons()
return _cache[name]
+def get_name_from_type(typ):
+ global _cache
+ if not _cache:
+ _reload_addons()
+ for addon in _cache.keys():
+ if typ == _cache[addon]['class']:
+ return addon
+ return None
+
# vim:set ts=4 sts=4 sw=4 et:
diff --git a/tutorius/core.py b/tutorius/core.py
index b24b80b..bfbe07b 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -24,9 +24,9 @@ This module contains the core classes for tutorius
import logging
import os
-from sugar.tutorius.TProbe import ProbeManager
-from sugar.tutorius.dbustools import save_args
-from sugar.tutorius import addon
+from .TProbe import ProbeManager
+from .dbustools import save_args
+from . import addon
logger = logging.getLogger("tutorius")
@@ -505,10 +505,9 @@ class FiniteStateMachine(State):
#TODO : Move this code inside the State itself - we're breaking
# encap :P
- if st._transitions:
- for event, state in st._transitions.items():
- if state == state_name:
- del st._transitions[event]
+ for event in st._transitions:
+ if st._transitions[event] == state_name:
+ del st._transitions[event]
# Remove the state from the dictionary
del self._states[state_name]
diff --git a/tutorius/creator.py b/tutorius/creator.py
index d56fc72..c477056 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -22,16 +22,19 @@ the activity itself.
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk.gdk
+import gtk.glade
import gobject
from gettext import gettext as T
-from sugar.graphics.toolbutton import ToolButton
+import os
+from sugar.graphics import icon
+import copy
-from sugar.tutorius import overlayer, gtkutils, actions, vault, properties, addon
-from sugar.tutorius import filters
-from sugar.tutorius.services import ObjectStore
-from sugar.tutorius.linear_creator import LinearCreator
-from sugar.tutorius.core import Tutorial
+from . import overlayer, gtkutils, actions, vault, properties, addon
+from . import filters
+from .services import ObjectStore
+from .core import Tutorial, FiniteStateMachine, State
+from . import viewer
class Creator(object):
"""
@@ -47,80 +50,162 @@ class Creator(object):
"""
self._activity = activity
if not tutorial:
- self._tutorial = LinearCreator()
+ self._tutorial = FiniteStateMachine('Untitled')
+ self._state = State(name='INIT')
+ self._tutorial.add_state(self._state)
+ self._state_counter = 1
else:
self._tutorial = tutorial
+ # TODO load existing tutorial; unused yet
self._action_panel = None
self._current_filter = None
self._intro_mask = None
self._intro_handle = None
- self._state_bubble = overlayer.TextBubble(self._tutorial.state_name)
allocation = self._activity.get_allocation()
self._width = allocation.width
self._height = allocation.height
self._selected_widget = None
self._eventmenu = None
+ self.tuto = None
+ self._guid = None
self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5))
self._activity._overlayer.put(self._hlmask, 0, 0)
- self._activity._overlayer.put(self._state_bubble,
- self._width/2-self._state_bubble.allocation.width/2, 0)
- self._state_bubble.show()
-
dlg_width = 300
dlg_height = 70
sw = gtk.gdk.screen_width()
sh = gtk.gdk.screen_height()
- self._tooldialog = gtk.Window()
- self._tooldialog.set_title("Tutorius tools")
- self._tooldialog.set_transient_for(self._activity)
- self._tooldialog.set_decorated(True)
- self._tooldialog.set_resizable(False)
- self._tooldialog.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY)
- self._tooldialog.set_destroy_with_parent(True)
- self._tooldialog.set_deletable(False)
- self._tooldialog.set_size_request(dlg_width, dlg_height)
-
- toolbar = gtk.Toolbar()
- for tool in addon.list_addons():
- meta = addon.get_addon_meta(tool)
- toolitem = ToolButton(meta['icon'])
- toolitem.set_tooltip(meta['display_name'])
- toolitem.connect("clicked", self._add_action_cb, tool)
- toolbar.insert(toolitem, -1)
- toolitem = ToolButton("go-next")
- toolitem.connect("clicked", self._add_step_cb)
- toolitem.set_tooltip("Add Step")
- toolbar.insert(toolitem, -1)
- toolitem = ToolButton("stop")
- toolitem.connect("clicked", self._cleanup_cb)
- toolitem.set_tooltip("End Tutorial")
- toolbar.insert(toolitem, -1)
- self._tooldialog.add(toolbar)
- self._tooldialog.show_all()
- # simpoir: I suspect the realized widget is a tiny bit larger than
- # it should be, thus the -10.
- self._tooldialog.move(sw-10-dlg_width, sh-dlg_height)
-
- self._propedit = EditToolBox(self._activity)
-
- def _evfilt_cb(self, menuitem, event_name, *args):
+
+ self._propedit = ToolBox(self._activity)
+ self._propedit.tree.signal_autoconnect({
+ 'on_quit_clicked': self._cleanup_cb,
+ 'on_save_clicked': self.save,
+ 'on_action_activate': self._add_action_cb,
+ 'on_event_activate': self._add_event_cb,
+ })
+ self._propedit.window.move(
+ gtk.gdk.screen_width()-self._propedit.window.get_allocation().width,
+ 100)
+
+
+ self._overview = viewer.Viewer(self._tutorial, self)
+ self._overview.win.set_transient_for(self._activity)
+
+ self._overview.win.move(0, gtk.gdk.screen_height()- \
+ self._overview.win.get_allocation().height)
+
+ self._transitions = dict()
+
+ def _update_next_state(self, state, event, next_state):
+ self._transitions[event] = next_state
+
+ evts = state.get_event_filter_list()
+ state.clear_event_filters()
+ for evt, next_state in evts:
+ state.add_event_filter(evt, self._transitions[evt])
+
+ def delete_action(self, action):
+ """
+ Removes the first instance of specified action from the tutorial.
+
+ @param action: the action object to remove from the tutorial
+ @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
+
+ return False
+
+ def delete_state(self):
+ """
+ Remove current state.
+ Limitation: The last state cannot be removed, as it doesn't have
+ any transitions to remove anyway.
+
+ @returns: True if successful, otherwise False.
+ """
+ if not self._state.get_event_filter_list():
+ # 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
+
+ def get_insertion_point(self):
+ return self._state.name
+
+ def set_insertion_point(self, state_name):
+ for action in self._state.get_action_list():
+ 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()
+ for action in state_actions:
+ action.enter_editmode()
+ action._drag._eventbox.connect_after(
+ "button-release-event", self._action_refresh_cb, action)
+
+ if state_actions:
+ self._propedit.action = state_actions[0]
+ else:
+ self._propedit.action = None
+
+
+ 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.
"""
- self.introspecting = False
- eventfilter = addon.create('GtkWidgetEventFilter',
- object_id=self._selected_widget,
- event_name=event_name)
# undo actions so they don't persist through step editing
- for action in self._tutorial.current_actions:
+ for action in self._state.get_action_list():
action.exit_editmode()
- self._tutorial.event(eventfilter)
- self._state_bubble.label = self._tutorial.state_name
self._hlmask.covered = None
self._propedit.action = None
self._activity.queue_draw()
@@ -159,67 +244,70 @@ class Creator(object):
self._eventmenu.popup(None, None, None, evt.button, evt.time)
self._activity.queue_draw()
- def set_intropecting(self, value):
- """
- Set whether creator is in UI introspection mode. Setting this will
- connect necessary handlers.
- @param value True to setup introspection handlers.
- """
- if bool(value) ^ bool(self._intro_mask):
- if value:
- self._intro_mask = overlayer.Mask(catch_events=True)
- self._intro_handle = self._intro_mask.connect_after(
- "button-press-event", self._intro_cb)
- self._activity._overlayer.put(self._intro_mask, 0, 0)
- else:
- self._intro_mask.catch_events = False
- self._intro_mask.disconnect(self._intro_handle)
- self._intro_handle = None
- self._activity._overlayer.remove(self._intro_mask)
- self._intro_mask = None
-
- def get_introspecting(self):
- """
- Whether creator is in UI introspection (catch all event) mode.
- @return True if introspection handlers are connected, or False if not.
- """
- return bool(self._intro_mask)
-
- introspecting = property(fset=set_intropecting, fget=get_introspecting)
-
- def _add_action_cb(self, widget, actiontype):
+ def _add_action_cb(self, widget, path):
"""Callback for the action creation toolbar tool"""
- action = addon.create(actiontype)
- if isinstance(action, actions.Action):
- action.enter_editmode()
- self._tutorial.action(action)
- # FIXME: replace following with event catching
- action._drag._eventbox.connect_after(
- "button-release-event", self._action_refresh_cb, action)
+ action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME]
+ action = addon.create(action_type)
+ action.enter_editmode()
+ self._state.add_action(action)
+ # FIXME: replace following with event catching
+ action._drag._eventbox.connect_after(
+ "button-release-event", self._action_refresh_cb, action)
+ self._overview.win.queue_draw()
+
+ def _add_event_cb(self, widget, path):
+ """Callback for the event creation toolbar tool"""
+ 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)
+ 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()
+
+ event_filters = self._state.get_event_filter_list()
+ 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()
else:
- addonname = type(action).__name__
- meta = addon.get_addon_meta(addonname)
- had_introspect = False
- for propname in meta['mandatory_props']:
- prop = getattr(type(action), propname)
- if isinstance(prop, properties.TUAMProperty):
- had_introspect = True
- self.introspecting = True
- elif isinstance(prop, properties.TStringProperty):
- dlg = TextInputDialog(text="Mandatory property",
- field=propname)
- setattr(action, propname, dlg.pop())
- elif isinstance(prop, properties.TIntProperty):
- dlg = TextInputDialog(text="Mandatory property",
- field=propname)
- setattr(action, propname, int(dlg.pop()))
- else:
- raise NotImplementedError()
-
- # FIXME: hack to reuse previous introspection code
- if not had_introspect:
- self._tutorial.event(action)
+ # 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
+
+ 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)
def _action_refresh_cb(self, widget, evt, action):
"""
@@ -234,44 +322,54 @@ class Creator(object):
"button-release-event", self._action_refresh_cb, action)
self._propedit.action = action
- def _add_step_cb(self, widget):
- """Callback for the "add step" tool"""
- self.introspecting = True
+ self._overview.win.queue_draw()
def _cleanup_cb(self, *args):
"""
Quit editing and cleanup interface artifacts.
"""
- self.introspecting = False
- eventfilter = filters.EventFilter()
# undo actions so they don't persist through step editing
- for action in self._tutorial.current_actions:
+ for action in self._state.get_action_list():
action.exit_editmode()
- self._tutorial.event(eventfilter)
- dlg = TextInputDialog(text=T("Enter a tutorial title."),
- field=T("Title"))
- tutorialName = ""
- while not tutorialName: tutorialName = dlg.pop()
- dlg.destroy()
-
- # prepare tutorial for serialization
- tuto = Tutorial(tutorialName, self._tutorial.fsm)
- bundle = vault.TutorialBundler()
- bundle.write_metadata_file(tuto)
- bundle.write_fsm(self._tutorial.fsm)
+ 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
self._activity._overlayer.remove(self._hlmask)
- self._activity._overlayer.remove(self._state_bubble)
self._hlmask.destroy()
self._hlmask = None
- self._tooldialog.destroy()
self._propedit.destroy()
+ self._overview.destroy()
self._activity.queue_draw()
del self._activity._creator
+ def save(self, widget=None):
+ if not self.tuto:
+ dlg = TextInputDialog(self._activity,
+ 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)
+
+
def launch(*args, **kwargs):
"""
Launch and attach a creator to the currently running activity.
@@ -281,46 +379,59 @@ class Creator(object):
activity._creator = Creator(activity)
launch = staticmethod(launch)
-class EditToolBox(gtk.Window):
- """Helper toolbox class for managing action properties"""
- def __init__(self, parent, action=None):
- """
- Create the property edition toolbox and display it.
+class ToolBox(object):
+ ICON_LABEL = 0
+ ICON_IMAGE = 1
+ ICON_NAME = 2
+ ICON_TIP = 3
+ def __init__(self, parent):
+ super(ToolBox, self).__init__()
+ self.__parent = parent
+ sugar_prefix = os.getenv("SUGAR_PREFIX",default="/usr")
+ glade_file = os.path.join(sugar_prefix, 'share', 'tutorius',
+ 'ui', 'creator.glade')
+ self.tree = gtk.glade.XML(glade_file)
+ self.window = self.tree.get_widget('mainwindow')
+ self._propbox = self.tree.get_widget('propbox')
+
+ self.window.set_transient_for(parent)
- @param parent the parent window of this toolbox, usually an activity
- @param action the action to introspect/edit
- """
- gtk.Window.__init__(self)
self._action = None
- self.__parent = parent # private avoid gtk clash
-
- self.set_title("Action Properties")
- self.set_transient_for(parent)
- self.set_decorated(True)
- self.set_resizable(False)
- self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY)
- self.set_destroy_with_parent(True)
- self.set_deletable(False)
- self.set_size_request(200, 400)
-
- self._vbox = gtk.VBox()
- self.add(self._vbox)
- propwin = gtk.ScrolledWindow()
- propwin.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
- propwin.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
- self._vbox.pack_start(propwin)
- self._propbox = gtk.VBox(spacing=10)
- propwin.add(self._propbox)
-
- self.action = action
-
- sw = gtk.gdk.screen_width()
- sh = gtk.gdk.screen_height()
-
- self.show_all()
- self.move(sw-10-200, (sh-400)/2)
-
- def refresh(self):
+ self.actions_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str)
+ self.actions_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING)
+ self.events_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str)
+ self.events_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING)
+
+ for toolname in addon.list_addons():
+ meta = addon.get_addon_meta(toolname)
+ iconfile = gtk.Image()
+ iconfile.set_from_file(icon.get_icon_file_name(meta['icon']))
+ img = iconfile.get_pixbuf()
+ label = format_multiline(meta['display_name'])
+
+ if meta['type'] == addon.TYPE_ACTION:
+ self.actions_list.append((label, img, toolname, meta['display_name']))
+ else:
+ self.events_list.append((label, img, toolname, meta['display_name']))
+
+ iconview_action = self.tree.get_widget('iconview1')
+ iconview_action.set_model(self.actions_list)
+ iconview_action.set_text_column(self.ICON_LABEL)
+ iconview_action.set_pixbuf_column(self.ICON_IMAGE)
+ iconview_action.set_tooltip_column(self.ICON_TIP)
+ iconview_event = self.tree.get_widget('iconview2')
+ iconview_event.set_model(self.events_list)
+ iconview_event.set_text_column(self.ICON_LABEL)
+ iconview_event.set_pixbuf_column(self.ICON_IMAGE)
+ iconview_event.set_tooltip_column(self.ICON_TIP)
+
+ self.window.show()
+
+ def destroy(self):
+ """ clean and free the toolbox """
+ self.window.destroy()
+
+ def refresh_properties(self):
"""Refresh property values from the selected action."""
if self._action is None:
return
@@ -333,6 +444,9 @@ class EditToolBox(gtk.Window):
if isinstance(prop, properties.TStringProperty):
propwdg = row.get_children()[1]
propwdg.get_buffer().set_text(propval)
+ elif isinstance(prop, properties.TUAMProperty):
+ propwdg = row.get_children()[1]
+ propwdg.set_label(propval)
elif isinstance(prop, properties.TIntProperty):
propwdg = row.get_children()[1]
propwdg.set_value(propval)
@@ -348,12 +462,10 @@ class EditToolBox(gtk.Window):
def set_action(self, action):
"""Setter for the action property."""
if self._action is action:
- self.refresh()
+ self.refresh_properties()
return
- parent = self._propbox.get_parent()
- parent.remove(self._propbox)
- self._propbox = gtk.VBox(spacing=10)
- parent.add(self._propbox)
+ for old_prop in self._propbox.get_children():
+ self._propbox.remove(old_prop)
self._action = action
if action is None:
@@ -368,6 +480,10 @@ class EditToolBox(gtk.Window):
propwdg.get_buffer().set_text(propval)
propwdg.connect_after("focus-out-event", \
self._str_prop_changed, action, propname)
+ elif isinstance(prop, properties.TUAMProperty):
+ propwdg = gtk.Button(propval)
+ propwdg.connect_after("clicked", \
+ self._uam_prop_changed, action, propname)
elif isinstance(prop, properties.TIntProperty):
adjustment = gtk.Adjustment(value=propval,
lower=prop.lower_limit.limit,
@@ -388,8 +504,8 @@ class EditToolBox(gtk.Window):
propwdg.set_text(str(propval))
row.pack_end(propwdg)
self._propbox.pack_start(row, expand=False)
- self._vbox.show_all()
- self.refresh()
+ self._propbox.show_all()
+ self.refresh_properties()
def get_action(self):
"""Getter for the action property"""
@@ -406,6 +522,11 @@ class EditToolBox(gtk.Window):
except ValueError:
widget.set_text(str(getattr(action, propname)[idx]))
self.__parent._creator._action_refresh_cb(None, None, action)
+ def _uam_prop_changed(self, widget, action, propname):
+ selector = WidgetSelector(self.__parent)
+ selection = selector.select()
+ setattr(action, propname, selection)
+ self.__parent._creator._action_refresh_cb(None, None, action)
def _str_prop_changed(self, widget, evt, action, propname):
buf = widget.get_buffer()
setattr(action, propname, buf.get_text(buf.get_start_iter(), buf.get_end_iter()))
@@ -414,9 +535,143 @@ class EditToolBox(gtk.Window):
setattr(action, propname, widget.get_value_as_int())
self.__parent._creator._action_refresh_cb(None, None, action)
+
+class WidgetSelector(object):
+ """
+ Allow selecting a widget from within a window without interrupting the
+ flow of the current call.
+
+ The selector will run on the specified window until either a widget
+ is selected or abort() gets called.
+ """
+ def __init__(self, window):
+ super(WidgetSelector, self).__init__()
+ self.window = window
+ self._intro_mask = None
+ self._intro_handle = None
+ self._select_handle = None
+ self._prelight = None
+
+ def select(self):
+ """
+ Starts selecting a widget, by grabbing control of the mouse and
+ highlighting hovered widgets until one is clicked.
+ @returns: a widget address or None
+ """
+ if not self._intro_mask:
+ self._prelight = None
+ self._intro_mask = overlayer.Mask(catch_events=True)
+ self._select_handle = self._intro_mask.connect_after(
+ "button-press-event", self._end_introspect)
+ self._intro_handle = self._intro_mask.connect_after(
+ "motion-notify-event", self._intro_cb)
+ self.window._overlayer.put(self._intro_mask, 0, 0)
+ self.window._overlayer.queue_draw()
+
+ while bool(self._intro_mask) and not gtk.main_iteration():
+ pass
+
+ return gtkutils.raddr_lookup(self._prelight)
+
+ def _end_introspect(self, widget, evt):
+ if evt.type == gtk.gdk.BUTTON_PRESS and self._prelight:
+ self._intro_mask.catch_events = False
+ self._intro_mask.disconnect(self._intro_handle)
+ self._intro_handle = None
+ self._intro_mask.disconnect(self._select_handle)
+ self._select_handle = None
+ self.window._overlayer.remove(self._intro_mask)
+ self._intro_mask = None
+ # for some reason, gtk may not redraw after this unless told to.
+ self.window.queue_draw()
+
+ def _intro_cb(self, widget, evt):
+ """
+ Callback for capture of widget events, when in introspect mode.
+ """
+ # widget has focus, let's hilight it
+ win = gtk.gdk.display_get_default().get_window_at_pointer()
+ if not win:
+ return
+ click_wdg = win[0].get_user_data()
+ if not click_wdg.is_ancestor(self.window._overlayer):
+ # as popups are not (yet) supported, it would break
+ # badly if we were to play with a widget not in the
+ # hierarchy.
+ return
+ for hole in self._intro_mask.pass_thru:
+ self._intro_mask.mask(hole)
+ self._intro_mask.unmask(click_wdg)
+ self._prelight = click_wdg
+
+ self.window.queue_draw()
+
+ def abort(self):
+ """
+ Ends the selection. The control will return to the select() caller
+ with a return value of None, as selection was aborted.
+ """
+ self._intro_mask.catch_events = False
+ self._intro_mask.disconnect(self._intro_handle)
+ self._intro_handle = None
+ self._intro_mask.disconnect(self._select_handle)
+ self._select_handle = None
+ self.window._overlayer.remove(self._intro_mask)
+ self._intro_mask = None
+ self._prelight = None
+
+class SignalInputDialog(gtk.MessageDialog):
+ def __init__(self, parent, text, field, addr):
+ """
+ Create a gtk signal selection dialog.
+
+ @param parent: the parent window this dialog should stay over.
+ @param text: the title of the dialog.
+ @param field: the field description of the dialog.
+ @param addr: the widget address from which to fetch signal list.
+ """
+ gtk.MessageDialog.__init__(self, parent,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+ gtk.MESSAGE_QUESTION,
+ gtk.BUTTONS_OK,
+ None)
+ self.set_markup(text)
+ self.model = gtk.ListStore(str)
+ widget = gtkutils.find_widget(parent, addr)
+ for signal_name in gobject.signal_list_names(widget):
+ self.model.append(row=(signal_name,))
+ self.entry = gtk.ComboBox(self.model)
+ cell = gtk.CellRendererText()
+ self.entry.pack_start(cell)
+ self.entry.add_attribute(cell, 'text', 0)
+ hbox = gtk.HBox()
+ lbl = gtk.Label(field)
+ hbox.pack_start(lbl, False)
+ hbox.pack_end(self.entry)
+ self.vbox.pack_end(hbox, True, True)
+ self.show_all()
+
+ def pop(self):
+ """
+ Show the dialog. It will run in it's own loop and return control
+ to the caller when a signal has been selected.
+
+ @returns: a signal name or None if no signal was selected
+ """
+ self.run()
+ self.hide()
+ iter = self.entry.get_active_iter()
+ if iter:
+ text = self.model.get_value(iter, 0)
+ return text
+ return None
+
+ def _dialog_done_cb(self, entry, response):
+ self.response(response)
+
class TextInputDialog(gtk.MessageDialog):
- def __init__(self, text, field):
- gtk.MessageDialog.__init__(self, None,
+ def __init__(self, parent, text, field):
+ gtk.MessageDialog.__init__(self, parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_QUESTION,
gtk.BUTTONS_OK,
@@ -440,4 +695,39 @@ class TextInputDialog(gtk.MessageDialog):
def _dialog_done_cb(self, entry, response):
self.response(response)
+# The purpose of this function is to reformat text, as current IconView
+# implentation does not insert carriage returns on long lines.
+# To preserve layout, this call reformat text to fit in small space under an
+# icon.
+def format_multiline(text, length=10, lines=3, line_separator='\n'):
+ """
+ Reformat a text to fit in a small space.
+
+ @param length: maximum char per line
+ @param lines: maximum number of lines
+ """
+ words = text.split(' ')
+ line = list()
+ return_val = []
+ linelen = 0
+
+ for word in words:
+ t_len = linelen+len(word)
+ if t_len < length:
+ line.append(word)
+ linelen = t_len+1 # count space
+ else:
+ if len(return_val)+1 < lines:
+ return_val.append(' '.join(line))
+ line = list()
+ linelen = 0
+ line.append(word)
+ else:
+ return_val.append(' '.join(line+['...']))
+ return line_separator.join(return_val)
+
+ return_val.append(' '.join(line))
+ return line_separator.join(return_val)
+
+
# vim:set ts=4 sts=4 sw=4 et:
diff --git a/tutorius/editor.py b/tutorius/editor.py
index 42cc718..9d2effe 100644
--- a/tutorius/editor.py
+++ b/tutorius/editor.py
@@ -24,7 +24,7 @@ import gobject
from gettext import gettext as _
-from sugar.tutorius.gtkutils import register_signals_numbered, get_children
+from .gtkutils import register_signals_numbered, get_children
class WidgetIdentifier(gtk.Window):
"""
diff --git a/tutorius/engine.py b/tutorius/engine.py
index 9c1dae4..e77a018 100644
--- a/tutorius/engine.py
+++ b/tutorius/engine.py
@@ -1,10 +1,10 @@
import logging
import dbus.mainloop.glib
from jarabe.model import shell
-
-from sugar.tutorius.vault import Vault
from sugar.bundle.activitybundle import ActivityBundle
+from .vault import Vault
+
class Engine:
"""
Driver for the execution of tutorials
diff --git a/tutorius/filters.py b/tutorius/filters.py
index 44621d5..38cf86b 100644
--- a/tutorius/filters.py
+++ b/tutorius/filters.py
@@ -18,7 +18,7 @@
import logging
logger = logging.getLogger("filters")
-from sugar.tutorius import properties
+from . import properties
class EventFilter(properties.TPropContainer):
diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py
index 78e94ce..f664c49 100644
--- a/tutorius/linear_creator.py
+++ b/tutorius/linear_creator.py
@@ -15,12 +15,12 @@
# 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 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
diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py
index 0a3d542..b967739 100644
--- a/tutorius/overlayer.py
+++ b/tutorius/overlayer.py
@@ -58,7 +58,7 @@ class Overlayer(gtk.Layout):
@param overlayed widget to be overlayed. Will be resized to full size.
"""
def __init__(self, overlayed=None):
- gtk.Layout.__init__(self)
+ super(Overlayer, self).__init__()
self._overlayed = overlayed
if overlayed:
@@ -83,7 +83,7 @@ class Overlayer(gtk.Layout):
if hasattr(child, "draw_with_context"):
# if the widget has the CanvasDrawable protocol, use it.
child.no_expose = True
- gtk.Layout.put(self, child, x, y)
+ super(Overlayer, self).put(child, x, y)
# be sure to redraw or the overlay may not show
self.queue_draw()
diff --git a/tutorius/properties.py b/tutorius/properties.py
index cbb2ae3..a675ba9 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -19,12 +19,12 @@ TutoriusProperties have the same behaviour as python properties (assuming you
also use the TPropContainer), with the added benefit of having builtin dialog
prompts and constraint validation.
"""
+from copy import copy
-from sugar.tutorius.constraints import Constraint, \
+from .constraints import Constraint, \
UpperLimitConstraint, LowerLimitConstraint, \
MaxSizeConstraint, MinSizeConstraint, \
ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint
-from copy import copy
class TPropContainer(object):
"""
@@ -310,6 +310,8 @@ class TUAMProperty(TutoriusProperty):
self.type = "uam"
+ self.default = self.validate(value)
+
class TAddonProperty(TutoriusProperty):
"""
Reprensents an embedded tutorius Addon Component (action, trigger, etc.)
@@ -331,6 +333,16 @@ class TAddonProperty(TutoriusProperty):
return super(TAddonProperty, self).validate(value)
raise ValueError("Expected TPropContainer instance as TaddonProperty value")
+class TEventType(TutoriusProperty):
+ """
+ Represents an GUI signal for a widget.
+ """
+ def __init__(self, value):
+ super(TEventType, self).__init__()
+ self.type = "gtk-signal"
+
+ self.default = self.validate(value)
+
class TAddonListProperty(TutoriusProperty):
"""
Reprensents an embedded tutorius Addon List Component.
diff --git a/tutorius/service.py b/tutorius/service.py
index 21f0cf1..eb246a1 100644
--- a/tutorius/service.py
+++ b/tutorius/service.py
@@ -1,7 +1,7 @@
-from engine import Engine
import dbus
-from dbustools import remote_call
+from .engine import Engine
+from .dbustools import remote_call
_DBUS_SERVICE = "org.tutorius.Service"
_DBUS_PATH = "/org/tutorius/Service"
diff --git a/tutorius/vault.py b/tutorius/vault.py
index bcaf5f1..b455a52 100644
--- a/tutorius/vault.py
+++ b/tutorius/vault.py
@@ -28,11 +28,11 @@ import uuid
import xml.dom.minidom
from xml.dom import NotFoundErr
import zipfile
-
-from sugar.tutorius import addon
-from sugar.tutorius.core import Tutorial, State, FiniteStateMachine
from ConfigParser import SafeConfigParser
+from . import addon
+from .core import Tutorial, State, FiniteStateMachine
+
logger = logging.getLogger("tutorius")
# this is where user installed/generated tutorials will go
diff --git a/tutorius/viewer.py b/tutorius/viewer.py
new file mode 100644
index 0000000..272558e
--- /dev/null
+++ b/tutorius/viewer.py
@@ -0,0 +1,423 @@
+# 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
+"""
+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 cairo
+from math import pi as PI
+PI2 = PI/2
+
+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
+
+# FIXME ideally, apps scale correctly and we should use proportional positions
+X_WIDTH = 800
+X_HEIGHT = 600
+ACTION_WIDTH = 100
+ACTION_HEIGHT = 70
+
+# block look
+BLOCK_PADDING = 5
+BLOCK_WIDTH = 100
+BLOCK_CORNERS = 10
+BLOCK_INNER_PAD = 10
+
+SNAP_WIDTH = BLOCK_WIDTH - BLOCK_PADDING - BLOCK_INNER_PAD*2
+SNAP_HEIGHT = SNAP_WIDTH*X_HEIGHT/X_WIDTH
+SNAP_SCALE = float(SNAP_WIDTH)/X_WIDTH
+
+class Viewer(object):
+ """
+ Renders a tutorial as a sequence of blocks, each block representing either
+ an action or an event (transition).
+
+ Current Viewer implementation lacks viewport management;
+ having many objects in a tutorial will not render properly.
+ """
+ def __init__(self, tutorial, creator):
+ super(Viewer, self).__init__()
+
+ self._tutorial = tutorial
+ self._creator = creator
+ self.alloc = None
+ self.click_pos = None
+ self.drag_pos = None
+ self.selection = []
+
+ self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.win.set_size_request(400, 200)
+ self.win.set_gravity(gtk.gdk.GRAVITY_SOUTH_WEST)
+ self.win.show()
+ self.win.set_deletable(False)
+ self.win.move(0, 0)
+
+ vbox = gtk.ScrolledWindow()
+ self.win.add(vbox)
+
+ 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.add_events(gtk.gdk.BUTTON_PRESS_MASK \
+ |gtk.gdk.BUTTON_MOTION_MASK \
+ |gtk.gdk.BUTTON_RELEASE_MASK \
+ |gtk.gdk.KEY_PRESS_MASK)
+ canvas.connect('button-press-event', self._on_click)
+ # drag-select disabled, for now
+ #canvas.connect('motion-notify-event', self._on_drag)
+ canvas.connect('button-release-event', self._on_drag_end)
+ canvas.connect('key-press-event', self._on_key_press)
+
+ canvas.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS)
+ canvas.grab_focus()
+
+ self.win.show_all()
+ canvas.set_size_request(2048, 180) # FIXME
+
+ def destroy(self):
+ self.win.destroy()
+
+
+ def _paint_state(self, ctx, states):
+ """
+ Paints a tutorius fsm state in a cairo context.
+ Final context state will be shifted by the size of the graphics.
+ """
+ block_width = BLOCK_WIDTH - BLOCK_PADDING
+ block_max_height = self.alloc.height
+
+ new_insert_point = None
+ cur_state = '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:
+ new_app = 'Calculate'
+ if new_app != cur_app:
+ ctx.save()
+ ctx.set_matrix(app_start)
+ self._render_app_hints(ctx, cur_app)
+ ctx.restore()
+ app_start = ctx.get_matrix()
+ ctx.translate(BLOCK_PADDING, 0)
+ cur_app = new_app
+
+ action_list = state.get_action_list()
+ if action_list:
+ local_height = (block_max_height - BLOCK_PADDING)/len(action_list) - BLOCK_PADDING
+ ctx.save()
+ for 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.render_action(ctx, block_width, local_height, action)
+ ctx.translate(0, local_height+BLOCK_PADDING)
+
+ ctx.restore()
+ ctx.translate(BLOCK_WIDTH, 0)
+
+ # insertion cursor painting made from two opposed triangles
+ # joined by a line.
+ if state.name == self._creator.get_insertion_point():
+ ctx.save()
+ bp2 = BLOCK_PADDING/2
+ ctx.move_to(-bp2, 0)
+ ctx.line_to(-BLOCK_PADDING-bp2, -BLOCK_PADDING)
+ ctx.line_to(bp2, -BLOCK_PADDING)
+ ctx.line_to(-bp2, 0)
+
+ ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING)
+ ctx.line_to(bp2, block_max_height-BLOCK_PADDING)
+ ctx.line_to(-BLOCK_PADDING-bp2, block_max_height-BLOCK_PADDING)
+ ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING)
+
+ ctx.line_to(-bp2, BLOCK_PADDING)
+ ctx.set_source_rgb(1.0, 1.0, 0.0)
+ ctx.stroke_preserve()
+ ctx.fill()
+ ctx.restore()
+
+
+ event_list = state.get_event_filter_list()
+ if event_list:
+ local_height = (block_max_height - BLOCK_PADDING)/len(event_list) - BLOCK_PADDING
+ ctx.save()
+ for event, next_state 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)
+ 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
+
+ if event_list:
+ try:
+ state = states[cur_state]
+ except KeyError:
+ break
+ yield True
+ else:
+ break
+
+ ctx.set_matrix(app_start)
+ self._render_app_hints(ctx, cur_app)
+
+ if self.click_pos:
+ if not new_insert_point:
+ new_insert_point = state
+
+ self._creator.set_insertion_point(new_insert_point.name)
+
+ yield False
+
+ def _render_snapshot(self, ctx, elem):
+ """
+ Render the "simplified screenshot-like" representation of elements positions.
+ """
+ ctx.set_source_rgba(1.0, 1.0, 1.0, 0.5)
+ ctx.rectangle(0, 0, SNAP_WIDTH, SNAP_HEIGHT)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ if hasattr(elem, 'position'):
+ pos = elem.position
+ # FIXME this size approximation is fine, but I believe we could
+ # do better.
+ ctx.scale(SNAP_SCALE, SNAP_SCALE)
+ ctx.rectangle(pos[0], pos[1], ACTION_WIDTH, ACTION_HEIGHT)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ def _render_app_hints(self, ctx, appname):
+ """
+ Fetches the icon of the app related to current states and renders it on a
+ separator, between states.
+ """
+ ctx.set_source_rgb(0.0, 0.0, 0.0)
+ ctx.set_dash((1,1,0,0), 1)
+ ctx.move_to(0, 0)
+ ctx.line_to(0, self.alloc.height)
+ ctx.stroke()
+ ctx.set_dash(tuple(), 1)
+
+ bundle_path = os.getenv("SUGAR_BUNDLE_PATH")
+ if bundle_path:
+ icon_path = activitybundle.ActivityBundle(bundle_path).get_icon()
+ icon = rsvg.Handle(icon_path)
+ ctx.save()
+ ctx.translate(-15, 0)
+ ctx.scale(0.5, 0.5)
+ icon_surf = icon.render_cairo(ctx)
+ ctx.restore()
+
+
+ def render_action(self, ctx, width, height, action):
+ """
+ Renders the action block, along with the icon of the action tool.
+ """
+ ctx.save()
+ inner_width = width-(BLOCK_CORNERS<<1)
+ inner_height = height-(BLOCK_CORNERS<<1)
+
+ paint_border = ctx.rel_line_to
+ filling = cairo.LinearGradient(0, 0, 0, inner_height)
+ if action not in self.selection:
+ filling.add_color_stop_rgb(0.0, 0.7, 0.7, 1.0)
+ filling.add_color_stop_rgb(1.0, 0.1, 0.1, 0.8)
+ else:
+ filling.add_color_stop_rgb(0.0, 0.4, 0.4, 0.8)
+ filling.add_color_stop_rgb(1.0, 0.0, 0.0, 0.5)
+ tracing = cairo.LinearGradient(0, 0, 0, inner_height)
+ tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0)
+ tracing.add_color_stop_rgb(1.0, 0.2, 0.2, 0.2)
+
+ 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(BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI, -PI2)
+
+ ctx.set_source(tracing)
+ ctx.stroke_preserve()
+ ctx.set_source(filling)
+ ctx.fill()
+
+ addon_name = addon.get_name_from_type(type(action))
+ # 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)
+
+ ctx.restore()
+
+ ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2)
+ self._render_snapshot(ctx, action)
+
+ ctx.restore()
+
+ def render_event(self, ctx, width, height, event):
+ """
+ Renders the action block, along with the icon of the action tool.
+ """
+ ctx.save()
+ inner_width = width-(BLOCK_CORNERS<<1)
+ inner_height = height-(BLOCK_CORNERS<<1)
+
+ filling = cairo.LinearGradient(0, 0, 0, inner_height)
+ if event not in self.selection:
+ filling.add_color_stop_rgb(0.0, 1.0, 0.8, 0.6)
+ filling.add_color_stop_rgb(1.0, 1.0, 0.6, 0.2)
+ else:
+ filling.add_color_stop_rgb(0.0, 0.8, 0.6, 0.4)
+ filling.add_color_stop_rgb(1.0, 0.6, 0.4, 0.1)
+ tracing = cairo.LinearGradient(0, 0, 0, inner_height)
+ tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0)
+ tracing.add_color_stop_rgb(1.0, 0.3, 0.3, 0.3)
+
+ ctx.move_to(BLOCK_CORNERS, 0)
+ ctx.rel_line_to(inner_width, 0)
+ ctx.rel_line_to(BLOCK_CORNERS, BLOCK_CORNERS)
+ ctx.rel_line_to(0, inner_height)
+ ctx.rel_line_to(-BLOCK_CORNERS, BLOCK_CORNERS)
+ ctx.rel_line_to(-inner_width, 0)
+ ctx.rel_line_to(-BLOCK_CORNERS, -BLOCK_CORNERS)
+ ctx.rel_line_to(0, -inner_height)
+ ctx.close_path()
+
+ ctx.set_source(tracing)
+ ctx.stroke_preserve()
+ ctx.set_source(filling)
+ ctx.fill()
+
+ 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)
+
+ 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):
+ """
+ Expose signal handler for the viewer's DrawingArea.
+ This loops through states 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
+ """
+ ctx = widget.window.cairo_create()
+ self.alloc = widget.get_allocation()
+ ctx.set_source_pixmap(widget.window,
+ widget.allocation.x,
+ widget.allocation.y)
+
+ # draw no more than our expose event intersects our child
+ region = gtk.gdk.region_rectangle(widget.allocation)
+ r = gtk.gdk.region_rectangle(evt.area)
+ region.intersect(r)
+ ctx.region (region)
+ ctx.clip()
+ ctx.paint()
+
+ ctx.translate(BLOCK_PADDING, BLOCK_PADDING)
+
+ painter = self._paint_state(ctx, states)
+ while painter.next(): pass
+
+ if self.click_pos and self.drag_pos:
+ ctx.set_matrix(cairo.Matrix())
+ ctx.rectangle(self.click_pos[0], self.click_pos[1],
+ self.drag_pos[0]-self.click_pos[0],
+ self.drag_pos[1]-self.click_pos[1])
+ ctx.set_source_rgba(0, 0, 1, 0.5)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ return False
+
+ def _on_click(self, widget, evt):
+ # the rendering pipeline will work out the click validation process
+ self.drag_pos = None
+ self.drag_pos = self.click_pos = evt.get_coords()
+ widget.queue_draw()
+
+ self.selection = []
+
+ def _on_drag(self, widget, evt):
+ self.drag_pos = evt.get_coords()
+ widget.queue_draw()
+
+ def _on_drag_end(self, widget, evt):
+ self.click_pos = self.drag_pos = None
+ widget.queue_draw()
+
+ 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()
+ else:
+ self._creator.delete_action(selected)
+ widget.queue_draw()
+
+