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-12-04 02:21:14 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-12-04 02:21:14 (GMT)
commit49ab39870266196f6267f83dd6951f839ebde773 (patch)
tree116fb1552882edf5b4e42bbcc293c5ec918abca4
parentb8c9419f0381c1864035af76853ed1c02ad434cc (diff)
parent243d29608df4816f791c98407c10fa8a702574e1 (diff)
Merge ../../simpoirs-clone into remote_integration
Conflicts: src/extensions/tutoriusremote.py
-rw-r--r--addons/bubblemessage.py2
-rw-r--r--addons/bubblemessagewimg.py2
-rw-r--r--addons/gtkwidgeteventfilter.py1
-rw-r--r--addons/gtkwidgettypefilter.py3
-rw-r--r--data/ui/creator.glade50
-rwxr-xr-xsrc/extensions/tutoriusremote.py64
-rw-r--r--tests/probetests.py25
-rw-r--r--tutorius/TProbe.py177
-rw-r--r--tutorius/actions.py53
-rw-r--r--tutorius/creator.py423
-rw-r--r--tutorius/properties.py33
-rw-r--r--tutorius/translator.py6
-rw-r--r--tutorius/viewer.py3
13 files changed, 584 insertions, 258 deletions
diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py
index aaf086c..7e91d00 100644
--- a/addons/bubblemessage.py
+++ b/addons/bubblemessage.py
@@ -93,7 +93,7 @@ class BubbleMessage(Action):
self.overlay.put(self._bubble, x, y)
self._bubble.show()
- self._drag = DragWrapper(self._bubble, self.position, True)
+ self._drag = DragWrapper(self._bubble, self.position, update_action_cb=self.update_property, draggable=True)
def exit_editmode(self, *args):
if self._drag.moved:
diff --git a/addons/bubblemessagewimg.py b/addons/bubblemessagewimg.py
index 9c3dfc1..0ad444f 100644
--- a/addons/bubblemessagewimg.py
+++ b/addons/bubblemessagewimg.py
@@ -96,7 +96,7 @@ class BubbleMessageWImg(Action):
self.overlay.put(self._bubble, x, y)
self._bubble.show()
- self._drag = DragWrapper(self._bubble, self.position, True)
+ self._drag = DragWrapper(self._bubble, self.position, update_action_cb=self.update_property, draggable=True)
def exit_editmode(self, *args):
x,y = self._drag.position
diff --git a/addons/gtkwidgeteventfilter.py b/addons/gtkwidgeteventfilter.py
index b5ce9ae..ac14399 100644
--- a/addons/gtkwidgeteventfilter.py
+++ b/addons/gtkwidgeteventfilter.py
@@ -65,6 +65,5 @@ __event__ = {
"icon" : "player_play",
"class" : GtkWidgetEventFilter,
"mandatory_props" : ["object_id", "event_name"],
- "test" : True
}
diff --git a/addons/gtkwidgettypefilter.py b/addons/gtkwidgettypefilter.py
index 4ffecb5..8faf172 100644
--- a/addons/gtkwidgettypefilter.py
+++ b/addons/gtkwidgettypefilter.py
@@ -96,5 +96,6 @@ __event__ = {
'display_name' : 'Widget Filter',
'icon' : '',
'class' : GtkWidgetTypeFilter,
- 'mandatory_props' : ['next_state', 'object_id']
+ 'mandatory_props' : ['next_state', 'object_id'],
+ "test" : True,
}
diff --git a/data/ui/creator.glade b/data/ui/creator.glade
index 1c9669d..aeba19c 100644
--- a/data/ui/creator.glade
+++ b/data/ui/creator.glade
@@ -1,16 +1,19 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--Generated with glade3 3.4.5 on Sun Nov 1 16:39:50 2009 -->
<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="type">GTK_WINDOW_TOPLEVEL</property>
<property name="title" translatable="yes">Toolbox</property>
<property name="resizable">False</property>
- <property name="window_position">center-on-parent</property>
+ <property name="decorated">False</property>
+ <property name="window_position">GTK_WIN_POS_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="type_hint">GDK_WINDOW_TYPE_HINT_UTILITY</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="focus_on_map">False</property>
@@ -19,35 +22,37 @@
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<property name="spacing">5</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<widget class="GtkHButtonBox" id="hbuttonbox1">
<property name="visible">True</property>
<property name="spacing">5</property>
- <property name="layout_style">start</property>
+ <property name="layout_style">GTK_BUTTONBOX_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="label">gtk-save</property>
<property name="use_stock">True</property>
+ <property name="response_id">0</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="label">gtk-quit</property>
<property name="use_stock">True</property>
+ <property name="response_id">0</property>
<signal name="clicked" handler="on_quit_clicked"/>
</widget>
<packing>
@@ -60,24 +65,24 @@
<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>
+ <property name="hscrollbar_policy">GTK_POLICY_NEVER</property>
+ <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkViewport" id="viewport1">
<property name="visible">True</property>
- <property name="resize_mode">queue</property>
+ <property name="resize_mode">GTK_RESIZE_QUEUE</property>
<child>
<widget class="GtkVBox" id="vbox2">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<widget class="GtkExpander" id="expander1">
<property name="visible">True</property>
@@ -90,7 +95,6 @@
<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>
@@ -106,7 +110,6 @@
</widget>
<packing>
<property name="expand">False</property>
- <property name="position">0</property>
</packing>
</child>
<child>
@@ -121,7 +124,6 @@
<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>
@@ -153,8 +155,9 @@
<child>
<widget class="GtkVBox" id="propbox">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<property name="spacing">10</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<placeholder/>
</child>
@@ -169,26 +172,27 @@
<widget class="GtkHButtonBox" id="hbuttonbox2">
<property name="visible">True</property>
<property name="spacing">5</property>
- <property name="layout_style">start</property>
+ <property name="layout_style">GTK_BUTTONBOX_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="label">gtk-media-record</property>
<property name="use_stock">True</property>
+ <property name="response_id">0</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="label">gtk-media-stop</property>
<property name="use_stock">True</property>
+ <property name="response_id">0</property>
</widget>
<packing>
<property name="expand">False</property>
diff --git a/src/extensions/tutoriusremote.py b/src/extensions/tutoriusremote.py
index 918f3da..fc93659 100755
--- a/src/extensions/tutoriusremote.py
+++ b/src/extensions/tutoriusremote.py
@@ -36,7 +36,7 @@ from sugar.graphics.combobox import ComboBox
from jarabe.frame.frameinvoker import FrameWidgetInvoker
from jarabe.model.shell import get_model
-#from sugar.tutorius.creator import default_creator
+from sugar.tutorius.creator import default_creator
from sugar.tutorius.vault import Vault
@@ -45,12 +45,7 @@ _ICON_NAME = 'tutortool'
LOGGER = logging.getLogger('remote')
class TutoriusRemote(TrayIcon):
-
- FRAME_POSITION_RELATIVE = 102
-
- def __init__(self):#, creator):
- #self._creator = creator
-
+ def __init__(self):
client = gconf.client_get_default()
self._color = XoColor(client.get_string('/desktop/sugar/user/color'))
@@ -67,69 +62,74 @@ class TPalette(Palette):
def __init__(self, primary_text):
super(TPalette, self).__init__(primary_text)
- #self._creator_item = gtk.MenuItem(_('Create a tutorial'))
- #self._creator_item.connect('activate', self._start_creator)
- #self._creator_item.show()
+ self._creator_item = gtk.MenuItem(_('Create a tutorial'))
+ self._creator_item.connect('activate', self._toggle_creator)
+ self._creator_item.show()
self._tut_list_item = gtk.MenuItem(_('Show tutorials'))
self._tut_list_item.connect('activate', self._list_tutorials)
self._tut_list_item.show()
- #self.menu.append(self._creator_item)
+ self.menu.append(self._creator_item)
self.menu.append(self._tut_list_item)
self.set_content(None)
- #def _start_creator(self, widget):
- # default_creator().start_authoring(tutorial=None)
+ def _toggle_creator(self, widget):
+ creator = default_creator()
+
+ if creator.is_authoring == False:
+ # Replace the start creator button by the stop creator
+ # Allocate a white color for the text
+ self._creator_item.props.label = _("Stop this tutorial")
+ creator.start_authoring(tutorial=None)
+
+ else:
+ # Attempt to close the creator - this will popup a confirmation
+ # dialog if the user has unsaved changes
+ creator._cleanup_cb()
+
+ # If the creator was not actually closed - (in case cancel
+ # is implemented one day)
+ if creator.is_authoring == True:
+ return
+ # Switch back to start creator entry
+ self._creator_item.props.label = _("Create a tutorial")
def _list_tutorials(self, widget):
- # Create the selection dialog
dlg = gtk.Dialog('Run a tutorial',
None,
gtk.DIALOG_MODAL,
- (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
- gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
- # Write the user prompt
+ (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
+ gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
dlg.vbox.pack_start(gtk.Label(_('Which tutorial do you want to run?\n')))
- # Fetchthe current activity name
activity = get_model().get_active_activity()
- act_name = activity.get_activity_name()
- # Query the vault to get the tutorials related to this activity
+ act_name = activity.get_activity_name()
tutorial_dict = Vault.list_available_tutorials(act_name)
# Build the combo box
combo = ComboBox()
-
- # Insert all the related tutorials
for (tuto_name, tuto_guid) in tutorial_dict.items():
combo.append_item(tuto_name, tuto_guid)
dlg.vbox.pack_end(combo)
dlg.show_all()
- # Show the dialog to the user
result = dlg.run()
+ dlg.destroy()
- # If the user cliked OK
if result == gtk.RESPONSE_ACCEPT:
- # Get the current active item in the
row = combo.get_active_item()
if row:
- # Fetch the name and Guid
guid = row[0]
name = row[1]
-
LOGGER.debug("TPalette :: Got message to launch tutorial %s with guid %s"%(str(name), str(guid)))
- # Send a DBus message to the Engine to run this tutorial
from sugar.tutorius.service import ServiceProxy
service = ServiceProxy()
+
service.launch(guid)
-
- # Close the dialog
- dlg.destroy()
def setup(tray):
- tray.add_device(TutoriusRemote())#default_creator()))
+ tray.add_device(TutoriusRemote())
diff --git a/tests/probetests.py b/tests/probetests.py
index 37748d8..17c6afc 100644
--- a/tests/probetests.py
+++ b/tests/probetests.py
@@ -96,11 +96,13 @@ class MockProbeProxy(object):
def isAlive(self):
return self.MockAlive
- def install(self, action, action_installed_cb, error_cb):
+ def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None):
self.MockAction = action
self.MockAddressCallback_install = action_installed_cb
self.MockInstallErrorCallback = error_cb
self.MockActionUpdate = None
+ self.MockIsEditing = is_editing
+ self.MockEditCb = editing_cb
return None
def update(self, action_address, newaction, block=False):
@@ -108,9 +110,10 @@ class MockProbeProxy(object):
self.MockActionUpdate = newaction
return None
- def uninstall(self, action_address):
+ def uninstall(self, action_address, is_editing=False):
self.MockAction = None
self.MockActionUpdate = None
+ self.MockIsEditing = None
return None
def subscribe(self, event, notif_cb, subscribe_cb, error_cb):
@@ -223,35 +226,35 @@ class ProbeTest(unittest.TestCase):
assert message_box is None, "Message box should still be empty"
#install 1
- address = self.probe.install(pickle.dumps(action))
+ address = self.probe.install(pickle.dumps(action), False)
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))
+ address2 = self.probe.install(pickle.dumps(action), False)
assert message_box == (10, "ahhah!"), "message box should have changed"
assert address != address2, "action addresses should be different"
#uninstall 2
- self.probe.uninstall(address2)
+ self.probe.uninstall(address2, False)
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))
+ self.probe.update(address, pickle.dumps(action._props), False)
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.assertRaises(KeyError, self.probe.update, address2, pickle.dumps(action._props), False)
- self.probe.uninstall(address)
+ self.probe.uninstall(address, False)
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)
+ self.probe.uninstall(address, False)
assert message_box == "Test", "undo should not have happened again"
def test_events(self):
@@ -458,10 +461,10 @@ class ProbeProxyTest(unittest.TestCase):
#ErrorCase: Uninstall on not installed action (silent fail)
#Test the uninstall
- self.probeProxy.uninstall(action2_address)
+ self.probeProxy.uninstall(action2_address, False)
assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed"
- self.probeProxy.uninstall(address)
+ self.probeProxy.uninstall(address, False)
assert self.mockObj.MockCall["uninstall"]["args"][0] == address, "1 argument, the action address"
def test_events(self):
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
index d0cc6e1..c0eedee 100644
--- a/tutorius/TProbe.py
+++ b/tutorius/TProbe.py
@@ -20,16 +20,16 @@ import os
import gobject
-import dbus
import dbus.service
import cPickle as pickle
+from functools import partial
from . import addon
+from . import properties
from .services import ObjectStore
-from .properties import TPropContainer
-from .dbustools import save_args, ignore, logError
+from .dbustools import remote_call, save_args, ignore, logError
import copy
"""
@@ -127,11 +127,12 @@ class TProbe(dbus.service.Object):
# ------------------ Action handling --------------------------------------
@dbus.service.method("org.tutorius.ProbeInterface",
- in_signature='s', out_signature='s')
- def install(self, pickled_action):
+ in_signature='sb', out_signature='s')
+ def install(self, pickled_action, is_editing):
"""
Install an action on the Activity
@param pickled_action string pickled action
+ @param is_editing whether this action comes from the editor
@return string address of installed action
"""
loaded_action = pickle.loads(str(pickled_action))
@@ -144,17 +145,22 @@ class TProbe(dbus.service.Object):
if action._props:
action._props.update(loaded_action._props)
- action.do(activity=self._activity)
-
+ if not is_editing:
+ action.do(activity=self._activity)
+ else:
+ action.enter_editmode()
+ action.set_notification_cb(partial(self.update_action, address))
+
return address
@dbus.service.method("org.tutorius.ProbeInterface",
- in_signature='ss', out_signature='')
- def update(self, address, action_props):
+ in_signature='ssb', out_signature='')
+ def update(self, address, action_props, is_editing):
"""
Update an already registered action
@param address string address returned by install()
@param action_props pickled action properties
+ @param is_editing whether this action comes from the editor
@return None
"""
action = self._installedActions[address]
@@ -162,26 +168,47 @@ class TProbe(dbus.service.Object):
if action._props:
props = pickle.loads(str(action_props))
action._props.update(props)
- action.undo()
- action.do()
+ if not is_editing:
+ action.undo()
+ action.do()
+ else:
+ action.exit_editmode()
+ action.enter_editmode()
@dbus.service.method("org.tutorius.ProbeInterface",
- in_signature='s', out_signature='')
- def uninstall(self, address):
+ in_signature='sb', out_signature='')
+ def uninstall(self, address, is_editing):
"""
Uninstall an action
@param address string address returned by install()
+ @param is_editing whether this action comes from the editor
@return None
"""
if self._installedActions.has_key(address):
action = self._installedActions[address]
- action.undo()
+ if not is_editing:
+ action.undo()
+ else:
+ action.exit_editmode()
self._installedActions.pop(address)
# ------------------ Event handling ---------------------------------------
@dbus.service.method("org.tutorius.ProbeInterface",
in_signature='s', out_signature='s')
+ def create_event(self, addon_name):
+ # avoid recursive imports
+ event = addon.create(addon_name)
+ addonname = type(event).__name__
+ meta = addon.get_addon_meta(addonname)
+ for propname in meta['mandatory_props']:
+ prop = getattr(type(event), propname)
+ prop.widget_class.run_dialog(self._activity, event, propname)
+
+ return pickle.dumps(event)
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='s')
def subscribe(self, pickled_event):
"""
Subscribe to an Event
@@ -214,7 +241,6 @@ class TProbe(dbus.service.Object):
@param address string adress returned by subscribe()
@return None
"""
-
if self._subscribedEvents.has_key(address):
eventfilter = self._subscribedEvents[address]
eventfilter.remove_handlers()
@@ -235,6 +261,21 @@ class TProbe(dbus.service.Object):
else:
raise RuntimeWarning("Attempted to raise an unregistered event")
+ @dbus.service.signal("org.tutorius.ProbeInterface")
+ def addonUpdated(self, addon_address, pickled_diff_dict):
+ # Don't do any added processing, the signal will be sent
+ # when the method exits
+ pass
+
+ def update_action(self, addon_address, diff_dict):
+ LOGGER.debug("TProbe :: Trying to update action %s with new property dict %s"%(addon_address, str(diff_dict)))
+ # Check that this action is installed
+ if addon_address in self._installedActions.keys():
+ LOGGER.debug("TProbe :: Updating action %s"%(addon_address))
+ self.addonUpdated(addon_address, pickle.dumps(diff_dict))
+ else:
+ raise RuntimeWarning("Attempted to updated an action that wasn't installed")
+
# Return a unique name for this action
def _generate_action_reference(self, action):
# TODO elavoie 2009-07-25 Should return a universal address
@@ -282,6 +323,7 @@ class ProbeProxy:
self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface")
self._actions = {}
+ self._edition_callbacks = {}
# We keep those two data structures to be able to have multiple callbacks
# for the same event and be able to remove them independently
# _subscribedEvents holds a list of callback addresses's for each event
@@ -290,6 +332,17 @@ class ProbeProxy:
self._registeredCallbacks = {}
self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface")
+ self._object.connect_to_signal("addonUpdated", self._handle_update_signal, dbus_interface="org.tutorius.ProbeInterface")
+
+ def _handle_update_signal(self, addon_address, pickled_diff_dict):
+ address = str(addon_address)
+ diff_dict = pickle.loads(str(pickled_diff_dict))
+ LOGGER.debug("ProbeProxy :: Received update property for action %s"%(address))
+ # Launch the callback to warn the upper layers of a modification of the addon
+ # from a widget inside the activity
+ if self._edition_callbacks.has_key(address):
+ LOGGER.debug("ProbeProxy :: Executing update callback...")
+ self._edition_callbacks[address](address, diff_dict)
def _handle_signal(self, pickled_event):
event = pickle.loads(str(pickled_event))
@@ -310,33 +363,47 @@ class ProbeProxy:
except:
return False
- def __update_action(self, action, callback, address):
+ def __update_action(self, action, callback, editing_cb, address):
LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address))
+ address = str(address)
+ # Store the action
self._actions[address] = action
+ # Store the edition callback
+ if editing_cb:
+ self._edition_callbacks[address] = editing_cb
+ # Propagate the action installed callback upwards in the stack
callback(address)
def __clear_action(self, address):
+ # Remove the action installed at this address
self._actions.pop(address, None)
+ # Remove the edition callback
+ self._edition_callbacks.pop(address, None)
- def install(self, action, action_installed_cb, error_cb):
+ def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None):
"""
Install an action on the TProbe's activity
@param action Action to install
@param action_installed_cb The callback function to call once the action is installed
@param error_cb The callback function to call when an error happens
+ @param is_editing whether this action comes from the editor
+ @param editing_cb The function to execute when the action is updated
+ (this is only done in edition mode)
@return None
"""
- self._probe.install(pickle.dumps(action),
- reply_handler=save_args(self.__update_action, action, action_installed_cb),
- error_handler=save_args(error_cb, action))
+ self._probe.install(pickle.dumps(action),
+ is_editing,
+ reply_handler=save_args(self.__update_action, action, action_installed_cb, editing_cb),
+ error_handler=save_args(error_cb, action))
- def update(self, action_address, newaction):
+ def update(self, action_address, newaction, is_editing=False):
"""
Update an already installed action's properties and run it again
@param action_address The address of the action to update. This is
provided by the install callback method.
@param newaction Action to update it with
@param block Force a synchroneous dbus call if True
+ @param is_editing whether this action comes from the editor
@return None
"""
#TODO review how to make this work well
@@ -344,19 +411,20 @@ class ProbeProxy:
raise RuntimeWarning("Action not installed")
#TODO Check error handling
return self._probe.update(action_address, pickle.dumps(newaction._props),
+ is_editing,
reply_handler=ignore,
error_handler=logError)
- def uninstall(self, action_address):
+ def uninstall(self, action_address, is_editing):
"""
Uninstall an installed action
@param action_address The address of the action to uninstall. This address was given
on action installation
- @param block Force a synchroneous dbus call if True
+ @param is_editing whether this action comes from the editor
"""
if action_address in self._actions:
self._actions.pop(action_address, None)
- self._probe.uninstall(action_address, reply_handler=ignore, error_handler=logError)
+ self._probe.uninstall(action_address, is_editing, reply_handler=ignore, error_handler=logError)
def __update_event(self, event, callback, event_subscribed_cb, address):
LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address))
@@ -407,7 +475,18 @@ class ProbeProxy:
else:
LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address)
+ def create_event(self, addon_name):
+ """
+ Create an event on the app side and request the user to fill the
+ properties before returning it.
+
+ @param addon_name: the add-on name of the event
+ @returns: an eventfilter instance
+ """
+ return pickle.loads(str(self._probe.create_event(addon_name)))
+
def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb):
+
"""
Register an event listener
@param event Event to listen for
@@ -427,7 +506,7 @@ class ProbeProxy:
reply_handler=save_args(self.__update_event, event, notification_cb, event_subscribed_cb),
error_handler=save_args(error_cb, event))
- def unsubscribe(self, address, block=True):
+ def unsubscribe(self, address):
"""
Unregister an event listener
@param address identifier given by subscribe()
@@ -449,12 +528,13 @@ class ProbeProxy:
subscribed events should be removed.
"""
for action_addr in self._actions.keys():
- self.uninstall(action_addr)
+ # TODO : Make sure there is a way for each action to be properly
+ # uninstalled according to its right edition mode
+ self.uninstall(action_addr, True)
for address in self._subscribedEvents.keys():
self.unsubscribe(address)
-
class ProbeManager(object):
"""
The ProbeManager provides multiplexing across multiple activity ProbeProxies
@@ -464,6 +544,8 @@ class ProbeManager(object):
"""
_LOGGER = logging.getLogger("sugar.tutorius.ProbeManager")
+ default_instance = None
+
def __init__(self, proxy_class=ProbeProxy):
"""Constructor
@param proxy_class Class to use for creating Proxies to activities.
@@ -478,6 +560,8 @@ class ProbeManager(object):
ProbeManager._LOGGER.debug("__init__()")
+ ProbeManager.default_instance = self
+
def setCurrentActivity(self, activity_id):
if not activity_id in self._probes:
raise RuntimeError("Activity not attached, id : %s"%activity_id)
@@ -488,41 +572,64 @@ class ProbeManager(object):
currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
- def install(self, action, action_installed_cb, error_cb):
+ def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None):
"""
Install an action on the current activity
@param action Action to install
@param action_installed_cb The callback to call once the action is installed
@param error_cb The callback that will be called if there is an error during installation
@param block Force a synchroneous dbus call if True
+ @param is_editing whether this action comes from the editor
+ @param editing_cb The function to execute when propagating changes on
+ this action (only used when is_editing is true)
@return None
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).install(action, action_installed_cb, error_cb)
+ return self._first_proxy(self.currentActivity).install(
+ action=action,
+ is_editing=is_editing,
+ action_installed_cb=action_installed_cb,
+ error_cb=error_cb,
+ editing_cb=editing_cb)
else:
raise RuntimeWarning("No activity attached")
- def update(self, action_address, newaction):
+ def update(self, action_address, newaction, is_editing=False):
"""
Update an already installed action's properties and run it again
@param action_address Action to update
@param newaction Action to update it with
@param block Force a synchroneous dbus call if True
+ @param is_editing whether this action comes from the editor
@return None
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).update(action_address, newaction)
+ return self._first_proxy(self.currentActivity).update(action_address, newaction, is_editing)
else:
raise RuntimeWarning("No activity attached")
- def uninstall(self, action_address):
+ def uninstall(self, action_address, is_editing=False):
"""
Uninstall an installed action
- @param action Action to uninstall
+ @param action_address Action to uninstall
@param block Force a synchroneous dbus call if True
+ @param is_editing whether this action comes from the editor
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).uninstall(action_address)
+ return self._first_proxy(self.currentActivity).uninstall(action_address, is_editing)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def create_event(self, addon_name):
+ """
+ Create an event on the app side and request the user to fill the
+ properties before returning it.
+
+ @param addon_name: the add-on name of the event
+ @returns: an eventfilter instance
+ """
+ if self.currentActivity:
+ return self._first_proxy(self.currentActivity).create_event(addon_name)
else:
raise RuntimeWarning("No activity attached")
@@ -599,8 +706,6 @@ class ProbeManager(object):
return self._probes[process_name]
else:
return []
-
-
def _first_proxy(self, process_name):
"""
diff --git a/tutorius/actions.py b/tutorius/actions.py
index 75c9c9b..a32138f 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -26,14 +26,21 @@ from . import addon
from .services import ObjectStore
from .properties import *
+import pickle
+
+import logging
+
+LOGGER = logging.getLogger("actions")
+
class DragWrapper(object):
"""Wrapper to allow gtk widgets to be dragged around"""
- def __init__(self, widget, position, draggable=False):
+ def __init__(self, widget, position, update_action_cb, draggable=False):
"""
Creates a wrapper to allow gtk widgets to be mouse dragged, if the
parent container supports the move() method, like a gtk.Layout.
@param widget the widget to enhance with drag capability
@param position the widget's position. Will translate the widget if needed
+ @param update_action_cb The callback to trigger
@param draggable wether to enable the drag functionality now
"""
self._widget = widget
@@ -45,6 +52,7 @@ class DragWrapper(object):
self.position = position # position of the widget
self.moved = False
+ self.update_action_cb = update_action_cb
self.draggable = draggable
def _pressed_cb(self, widget, evt):
@@ -79,10 +87,13 @@ class DragWrapper(object):
self._eventbox.grab_remove()
self._dragging = False
+ LOGGER.debug("DragWrapper :: Sending update notification...")
+ self.update_action_cb('position', self.position)
+
def _drag_end(self, *args):
"""Callback for end of drag (stolen focus)."""
self._dragging = False
-
+
def set_draggable(self, value):
"""Setter for the draggable property"""
if bool(value) ^ bool(self._drag_on):
@@ -139,6 +150,12 @@ class Action(TPropContainer):
TPropContainer.__init__(self)
self.position = (0,0)
self._drag = None
+ # The differences dictionary. This is a structure that holds all the
+ # modifications that were made to the properties since the action
+ # was last installed or the last moment the notification was executed.
+ # Every property change will be logged inside it and it will be sent
+ # to the creator to update its action edition dialog.
+ self._property_update_cb = None
def do(self, **kwargs):
"""
@@ -152,7 +169,35 @@ class Action(TPropContainer):
"""
pass #Should raise NotImplemented?
- def enter_editmode(self, **kwargs):
+
+ def set_notification_cb(self, notif_cb):
+ LOGGER.debug("Action :: Setting notification callback for creator...")
+ self._property_update_cb = notif_cb
+
+ def update_property(self, name, value):
+ """
+ Callback used in the wrapper to send a new value to an action.
+ """
+ LOGGER.debug("Action :: update_property on %s with value '%s'"%(name, str(value)))
+ #property = getattr(self, name)
+ #property = value
+
+ #self._props[name] = value
+ self.__setattr__(name, value)
+
+ # Send the notification to the creator
+ self.notify()
+
+ def notify(self):
+ LOGGER.debug("Action :: Notifying creator with new values in dict : %s"%(str(self._diff_dict)))
+ # If a notification callback was registered
+ if self._property_update_cb:
+ # Propagate it
+ self._property_update_cb(self._diff_dict)
+ # Empty the diff dict as we just synchronized with the creator
+ self._diff_dict.clear()
+
+ def enter_editmode(self, *args, **kwargs):
"""
Enters edit mode. The action should display itself in some way,
without affecting the currently running application. The default is
@@ -171,7 +216,7 @@ class Action(TPropContainer):
ObjectStore().activity._overlayer.put(self.__edit_img, x, y)
self.__edit_img.show_all()
- self._drag = DragWrapper(self.__edit_img, self.position, True)
+ self._drag = DragWrapper(self.__edit_img, self.position, update_action_cb=self.update_property, draggable=True)
def exit_editmode(self, **kwargs):
x, y = self._drag.position
diff --git a/tutorius/creator.py b/tutorius/creator.py
index 68c5fa6..0d3ac3c 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -21,67 +21,113 @@ the activity itself.
# along with this program; if not, write to the Free Software
# 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
+import pickle
+
import uuid
import os
from sugar.graphics import icon, style
+import jarabe.frame
-from . import overlayer, gtkutils, actions, vault, properties, addon
-from . import filters
+from . import overlayer, gtkutils, vault, addon
from .services import ObjectStore
-from .core import State
from .tutorial import Tutorial
from . import viewer
-from .propwidgets import TextInputDialog, StringPropWidget
+from .propwidgets import TextInputDialog
+from . import TProbe
+
+from functools import partial
+
+from dbus import SessionBus
+from dbus.service import method, Object, BusName
+from .dbustools import ignore
+
+import logging
+
+LOGGER = logging.getLogger("creator")
+
+BUS_PATH = "/org/tutorius/Creator"
+BUS_NAME = "org.tutorius.Creator"
+
+def default_creator():
+ """
+ The Creator class is a singleton. There can never be more than one creator
+ at a time. This method returns a new instance only if none
+ already exists. Else, the existing instance is returned.
+ """
+ Creator._instance = Creator._instance or Creator()
+ return Creator._instance
-class Creator(object):
+def get_creator_proxy():
"""
- Class acting as a bridge between the creator, serialization and core
- classes. This contains most of the UI part of the editor.
+ Returns a Creator dbus proxy for inter-process events.
"""
- def __init__(self, activity, tutorial=None):
+ bus = SessionBus()
+ proxy = bus.get_object(BUS_NAME, BUS_PATH)
+ return proxy
+
+class Creator(Object):
+ """
+ Class acting as a controller for the tutorial edition.
+ """
+
+ _instance = None
+
+ def __init__(self):
+ bus_name = BusName(BUS_NAME, bus=SessionBus())
+ Object.__init__(self, bus_name, BUS_PATH)
+
+ self.tuto = None
+ self.is_authoring = False
+ Creator._instance = self
+ self._probe_mgr = TProbe.ProbeManager.default_instance
+ self._installed_actions = list()
+
+ def start_authoring(self, tutorial=None):
"""
- Instanciate a tutorial creator for the activity.
+ Start authoring a tutorial.
- @param activity to bind the creator to
- @param tutorial an existing tutorial to edit, or None to create one
+ @type tutorial: str or None
+ @param tutorial: the unique identifier to an existing tutorial to
+ modify, or None to create a new one.
"""
- self._activity = activity
+ if self.is_authoring:
+ raise Exception("Already authoring")
+
+ self.is_authoring = True
+
if not tutorial:
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)
+ final_event = addon.create(
+ name='MessageButtonNext',
+ message=T('This is the end of this tutorial.')
+ )
+ self._tutorial.add_transition(
+ state_name=self._state,
+ transition=(final_event, self._tutorial.END),
+ )
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
- 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.metadata = None
- self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5))
- self._activity._overlayer.put(self._hlmask, 0, 0)
+ frame = jarabe.frame.get_view()
- dlg_width = 300
- dlg_height = 70
- sw = gtk.gdk.screen_width()
- sh = gtk.gdk.screen_height()
-
- self._propedit = ToolBox(self._activity)
+ self._propedit = ToolBox(None)
self._propedit.tree.signal_autoconnect({
'on_quit_clicked': self._cleanup_cb,
'on_save_clicked': self.save,
@@ -89,18 +135,39 @@ class Creator(object):
'on_event_activate': self._add_event_cb,
})
self._propedit.window.move(
- gtk.gdk.screen_width()-self._propedit.window.get_allocation().width,
- 100)
-
+ gtk.gdk.screen_width()-self._propedit.window.get_allocation().width\
+ -style.GRID_CELL_SIZE,
+ style.GRID_CELL_SIZE)
+ self._propedit.window.connect('enter-notify-event',
+ frame._enter_notify_cb)
+ self._propedit.window.connect('leave-notify-event',
+ frame._leave_notify_cb)
self._overview = viewer.Viewer(self._tutorial, self)
- self._overview.win.set_transient_for(self._activity)
+ self._overview.win.set_transient_for(frame._bottom_panel)
+ self._overview.win.connect('enter-notify-event',
+ frame._enter_notify_cb)
+ self._overview.win.connect('leave-notify-event',
+ frame._leave_notify_cb)
- self._overview.win.move(0, gtk.gdk.screen_height()- \
- self._overview.win.get_allocation().height)
+ self._overview.win.move(style.GRID_CELL_SIZE,
+ gtk.gdk.screen_height()-style.GRID_CELL_SIZE \
+ -self._overview.win.get_allocation().height)
self._transitions = dict()
+ # FIXME : remove when probemgr completed
+ #self._probe_mgr.attach('org.laptop.Calculate')
+ self._probe_mgr._current_activity = 'org.laptop.Calculate'
+
+ def _tool_enter_notify_cb(self, window, event):
+ frame = jarabe.frame.get_view()
+ frame._bottom_panel.hover = True
+
+ def _tool_leave_notify_cb(self, window, event):
+ frame = jarabe.frame.get_view()
+ frame._bottom_panel.hover = False
+
def _update_next_state(self, state, event, next_state):
self._transitions[event] = next_state
@@ -120,7 +187,8 @@ class Creator(object):
.get(action, None)
if not action_obj:
return False
- action_obj.exit_editmode()
+
+ self._probe_mgr.uninstall(action_obj.address)
self._tutorial.delete_action(action)
self._overview.win.queue_draw()
return True
@@ -133,7 +201,9 @@ class Creator(object):
@returns: True if successful, otherwise False.
"""
- if self._state in (self._tutorial.INIT, self._tutorial.END):
+ if self._state in (self._tutorial.INIT, self._tutorial.END) \
+ or self._tutorial.END in \
+ self._tutorial.get_following_states_dict(self._state):
# last state cannot be removed
return False
@@ -144,103 +214,105 @@ class Creator(object):
return bool(self._tutorial.delete_state(remove_state))
def get_insertion_point(self):
+ """
+ @returns: the current tutorial insertion point.
+ """
return self._state
def set_insertion_point(self, state_name):
- for action in self._tutorial.get_action_dict(self._state).values():
- action.exit_editmode()
+ """
+ Set the tutorial modification point to the specified state.
+ Actions of the state will enter the edit mode.
+ New actions will be inserted to that state and new transisions will
+ shift the current transision to the next state.
+
+ @param state_name: the name of the state to use as insertion point
+ """
+ # first is not modifiable, as the auto transition would make changes
+ # pointless. The end state is also pointless to modify, as the tutorial
+ # gets detached.
+ if state_name == self._tutorial.INIT \
+ or state_name == self._tutorial.END:
+ return
+
+ for action in self._installed_actions:
+ self._probe_mgr.uninstall(action.address,
+ is_editing=True)
+ self._installed_actions = []
self._state = state_name
state_actions = self._tutorial.get_action_dict(self._state).values()
+
for action in state_actions:
- action.enter_editmode()
- action._drag._eventbox.connect_after(
- "button-release-event", self._action_refresh_cb, action)
+ return_cb = partial(self._action_installed_cb, action)
+ self._probe_mgr.install(action,
+ action_installed_cb=return_cb,
+ error_cb=self._dbus_exception,
+ is_editing=True,
+ editing_cb=self.update_addon_property)
if state_actions:
+ # I'm really lazy right now and to keep things simple I simply
+ # always select the first action when
+ # we change state. we should really select the clicked block
+ # in the overview instead. FIXME
self._propedit.action = state_actions[0]
else:
self._propedit.action = None
self._overview.win.queue_draw()
-
- def _evfilt_cb(self, menuitem, event):
- """
- This will get called once the user has selected a menu item from the
- event filter popup menu. This should add the correct event filter
- to the FSM and increment states.
- """
- # undo actions so they don't persist through step editing
- for action in self._state.get_action_list():
- action.exit_editmode()
- self._hlmask.covered = None
- self._propedit.action = None
- self._activity.queue_draw()
-
- def _intro_cb(self, widget, evt):
- """
- Callback for capture of widget events, when in introspect mode.
- """
- if evt.type == gtk.gdk.BUTTON_PRESS:
- # widget has focus, let's hilight it
- win = gtk.gdk.display_get_default().get_window_at_pointer()
- click_wdg = win[0].get_user_data()
- if not click_wdg.is_ancestor(self._activity._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._selected_widget = gtkutils.raddr_lookup(click_wdg)
-
- if self._eventmenu:
- self._eventmenu.destroy()
- self._eventmenu = gtk.Menu()
- menuitem = gtk.MenuItem(label=type(click_wdg).__name__)
- menuitem.set_sensitive(False)
- self._eventmenu.append(menuitem)
- self._eventmenu.append(gtk.MenuItem())
-
- for item in gobject.signal_list_names(click_wdg):
- menuitem = gtk.MenuItem(label=item)
- menuitem.connect("activate", self._evfilt_cb, item)
- self._eventmenu.append(menuitem)
- self._eventmenu.show_all()
- self._eventmenu.popup(None, None, None, evt.button, evt.time)
- self._activity.queue_draw()
-
def _add_action_cb(self, widget, path):
"""Callback for the action creation toolbar tool"""
action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME]
+ LOGGER.debug("Creator :: Adding an action = %s"%(action_type))
action = addon.create(action_type)
- action.enter_editmode()
+ return_cb = partial(self._action_installed_cb, action)
+ self._probe_mgr.install(action,
+ action_installed_cb=return_cb,
+ error_cb=self._dbus_exception,
+ is_editing=True,
+ editing_cb=self.update_addon_property)
self._tutorial.add_action(self._state, action)
- # FIXME: replace following with event catching
- action._drag._eventbox.connect_after(
- "button-release-event", self._action_refresh_cb, action)
+ self._propedit.action = action
self._overview.win.queue_draw()
def _add_event_cb(self, widget, path):
- """Callback for the event creation toolbar tool"""
+ """
+ Callback for the event creation toolbar tool.
+
+ The behaviour of event addition is to push the transition of the current
+ state to the next (newly created state).
+
+ |
+ v
+ .--------. .-------. .--------.
+ | action |---->| event |---->| action |
+ '--------' '-------' '--------'
+ |
+ .--------. .-----------. v .-------. .--------.
+ | action |--->| new event |-->| event |---->| action |
+ '--------' '-----------' '-------' '--------'
+ The cursor always selects a state (between the action and transition)
+ The result is what the user expects: inserting before an action will
+ effectively shift the next transition to the next state.
+
+ """
event_type = self._propedit.events_list[path][ToolBox.ICON_NAME]
- event = addon.create(event_type)
- addonname = type(event).__name__
- meta = addon.get_addon_meta(addonname)
- for propname in meta['mandatory_props']:
- prop = getattr(type(event), propname)
- prop.widget_class.run_dialog(self._activity, event, propname)
+ event = self._probe_mgr.create_event(event_type)
event_filters = self._tutorial.get_transition_dict(self._state)
# if not at the end of tutorial
if event_filters:
- 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)
+ old_name = event_filters.keys()[0]
+ old_transition = self._tutorial.delete_transition(old_name)
+ new_state = self._tutorial.add_state(
+ transition_list=(old_transition,)
+ )
+ self._tutorial.add_transition(state_name=self._state,
+ transition=(event, new_state),
+ )
else:
# append empty state only if edit inserting at end of linearized
@@ -252,17 +324,29 @@ class Creator(object):
self.set_insertion_point(new_state)
+ def properties_changed(self, action, properties):
+ LOGGER.debug("Creator :: properties_changed for action at address %s to %s"%(action.address, str(properties)))
+ address = action.address
+ self._probe_mgr.update(address,
+ action,
+ is_editing=True)
+
+ def _update_error(self, exception):
+ pass
+
def _action_refresh_cb(self, widget, evt, action):
"""
Callback for refreshing properties values and notifying the
property dialog of the new values.
"""
- action.exit_editmode()
- action.enter_editmode()
- self._activity.queue_draw()
- # TODO: replace following with event catching
- action._drag._eventbox.connect_after(
- "button-release-event", self._action_refresh_cb, action)
+ self._probe_mgr.uninstall(action.address,
+ is_editing=True)
+ return_cb = partial(self._action_installed_cb, action)
+ self._probe_mgr.install(action,
+ action_installed_cb=return_cb,
+ error_cb=self._dbus_exception,
+ is_editing=True,
+ editing_cb=self.update_addon_property)
self._propedit.action = action
self._overview.win.queue_draw()
@@ -275,61 +359,116 @@ class Creator(object):
"""
# undo actions so they don't persist through step editing
for action in self._tutorial.get_action_dict(self._state).values():
- action.exit_editmode()
+ self._probe_mgr.uninstall(action.address,
+ is_editing=True)
+ # TODO : Support quit cancellation - right now,every time we execute this,
+ # we will forcibly end edition afterwards. It would be nice to keep creating
if kwargs.get('force', False):
dialog = gtk.MessageDialog(
- parent=self._activity,
+ parent=self._overview.win,
flags=gtk.DIALOG_MODAL,
type=gtk.MESSAGE_QUESTION,
buttons=gtk.BUTTONS_YES_NO,
- message_format=T('Do you want to save before stopping edition?'))
+ 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._hlmask.destroy()
- self._hlmask = None
self._propedit.destroy()
self._overview.destroy()
- self._activity.queue_draw()
- del self._activity._creator
+ self.is_authoring = False
def save(self, widget=None):
+ """
+ Save the currently edited tutorial to bundle, prompting for
+ a name as needed.
+ """
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()
+ tutorial_name = ""
+ while not tutorial_name:
+ tutorial_name = dlg.pop()
dlg.destroy()
self._metadata = {
vault.INI_GUID_PROPERTY: self._guid,
- vault.INI_NAME_PROPERTY: tutorialName,
+ vault.INI_NAME_PROPERTY: tutorial_name,
vault.INI_VERSION_PROPERTY: '1',
- 'activities':{os.environ['SUGAR_BUNDLE_NAME']:
- os.environ['SUGAR_BUNDLE_VERSION']
- },
}
+ # FIXME : The environment does not dispose of the appropriate
+ # variables to inform the creator at this point. We will
+ # need to iterate inside all the actions and remember
+ # their sources.
+
+ # FIXME : I insist. This is a hack.
+ related_activities_dict = {}
+ related_activities_dict['calculate'] = '27'
+
+ self._metadata['activities'] = dict(related_activities_dict)
vault.Vault.saveTutorial(self._tutorial, self._metadata)
+ def launch(self, *args):
+ assert False, "REMOVE THIS CALL!!!"
+ launch = staticmethod(launch)
- def launch(*args, **kwargs):
+ def _action_installed_cb(self, action, address):
"""
- Launch and attach a creator to the currently running activity.
+ This is a callback intented to be use to receive actions addresses
+ after they are installed.
+ @param address: the address of the newly installed action
"""
- activity = ObjectStore().activity
- if not hasattr(activity, "_creator"):
- activity._creator = Creator(activity)
- launch = staticmethod(launch)
+ action.address = address
+ self._installed_actions.append(action)
+
+ def _dbus_exception(self, event, exception):
+ """
+ This is a callback intented to be use to receive exceptions on remote
+ DBUS calls.
+ @param exception: the exception thrown by the remote process
+ """
+ LOGGER.debug("Creator :: Got exception -> %s"%(str(exception)))
+
+ @method(BUS_NAME,
+ in_signature='',
+ out_signature='b')
+ def get_authoring_state(self):
+ """
+ @returns True if the creator is being executed right now, False otherwise.
+ """
+ return self.is_authoring
+
+ def update_addon_property(self, addon_address, diff_dict):
+ """
+ Updates the properties on an addon.
+ @param addon_address The address of the addon that has the property
+ @param diff_dict The updates to apply to the property dict.
+ This is treated as a partial update to the addon's
+ dictionary and contains at least one property value pair
+ @returns True if the property was updated, False otherwise
+ """
+ # Look up the registered addresses inside the installed actions
+ for action in self._installed_actions:
+ # If this is the correct action
+ if action.address == addon_address:
+ # Update its property with the new value
+ action._props.update(diff_dict)
+ # Update the property edition dialog with it
+ self._propedit.action = action
+ return True
+
class ToolBox(object):
+ """
+ Palette window for edition tools, including the actions, states and
+ the editable property list of selected actions.
+ """
ICON_LABEL = 0
ICON_IMAGE = 1
ICON_NAME = 2
@@ -337,21 +476,26 @@ class ToolBox(object):
def __init__(self, parent):
super(ToolBox, self).__init__()
self.__parent = parent
- sugar_prefix = os.getenv("SUGAR_PREFIX",default="/usr")
+ 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.window.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_TOOLBAR_GREY.get_gdk_color())
self._propbox = self.tree.get_widget('propbox')
self._propedits = []
self.window.set_transient_for(parent)
+ self.window.set_keep_above(True)
self._action = None
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.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)
+ 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)
@@ -361,9 +505,13 @@ class ToolBox(object):
label = format_multiline(meta['display_name'])
if meta['type'] == addon.TYPE_ACTION:
- self.actions_list.append((label, img, toolname, meta['display_name']))
+ self.actions_list.append(
+ (label, img, toolname, meta['display_name'])
+ )
else:
- self.events_list.append((label, img, toolname, meta['display_name']))
+ self.events_list.append(
+ (label, img, toolname, meta['display_name'])
+ )
iconview_action = self.tree.get_widget('iconview1')
iconview_action.set_model(self.actions_list)
@@ -413,7 +561,8 @@ class ToolBox(object):
#Value field
prop = getattr(type(action), propname)
- propedit = prop.widget_class(self.__parent, action, propname, self._refresh_action_cb)
+ propedit = prop.widget_class(self.__parent, action, propname,
+ self._refresh_action_cb)
self._propedits.append(propedit)
row.pack_end(propedit.widget)
@@ -430,8 +579,8 @@ class ToolBox(object):
def _refresh_action_cb(self):
if self._action is not None:
- self.__parent._creator._action_refresh_cb(None, None, self._action)
-
+ default_creator().properties_changed(self._action, self._action._props)
+ #self.__parent._creator._action_refresh_cb(None, None, self._action)
# The purpose of this function is to reformat text, as current IconView
# implentation does not insert carriage returns on long lines.
diff --git a/tutorius/properties.py b/tutorius/properties.py
index a462782..85e8aa5 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -19,6 +19,7 @@ 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.
"""
+import uuid
from copy import copy, deepcopy
from .constraints import Constraint, \
@@ -35,6 +36,9 @@ from .propwidgets import PropWidget, \
FloatPropWidget, \
IntArrayPropWidget
+import logging
+LOGGER = logging.getLogger("properties")
+
class TPropContainer(object):
"""
A class containing properties. This does the attribute wrapping between
@@ -60,6 +64,14 @@ class TPropContainer(object):
self._props[attr_name] = propinstance.validate(
copy(propinstance.default))
+ self.__id = hash(uuid.uuid4())
+ # The differences dictionary. This is a structure that holds all the
+ # modifications that were made to the properties since the action
+ # was last installed or the last moment the notification was executed.
+ # Every property change will be logged inside it and it will be sent
+ # to the creator to update its action edition dialog.
+ self._diff_dict = {}
+
def __getattribute__(self, name):
"""
Process the 'fake' read of properties in the appropriate instance
@@ -93,8 +105,11 @@ class TPropContainer(object):
try:
# We attempt to get the property object with __getattribute__
# to work through inheritance and benefit of the MRO.
- return props.__setitem__(name,
+ real_value = props.__setitem__(name,
object.__getattribute__(self, name).validate(value))
+ LOGGER.debug("Action :: caching %s = %s in diff dict"%(name, str(value)))
+ self._diff_dict[name] = value
+ return real_value
except AttributeError:
return object.__setattr__(self, name, value)
@@ -128,21 +143,23 @@ class TPropContainer(object):
# Providing the hash methods necessary to use TPropContainers
# in a dictionary, according to their properties
def __hash__(self):
- #Return a hash of properties (key, value) sorted by key
- #We need to transform the list of property key, value lists into
- # a tuple of key, value tuples
- return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0])))))
+ # many places we use containers as keys to store additional data.
+ # Since containers are mutable, there is a need for a hash function
+ # where the result is constant, so we can still lookup old instances.
+ return self.__id
def __eq__(self, e2):
- return isinstance(e2, type(self)) and self._props == e2._props
+ return self.__id == e2.__id or \
+ (isinstance(e2, type(self)) and self._props == e2._props)
# Adding methods for pickling and unpickling an object with
# properties
def __getstate__(self):
- return self._props.copy()
+ return dict(id=self.__id, props=self._props.copy())
def __setstate__(self, dict):
- self._props.update(dict)
+ self.__id = dict['id']
+ self._props.update(dict['props'])
class TutoriusProperty(object):
"""
diff --git a/tutorius/translator.py b/tutorius/translator.py
index 4f29078..bd24f8f 100644
--- a/tutorius/translator.py
+++ b/tutorius/translator.py
@@ -177,7 +177,7 @@ class ResourceTranslator(object):
install_error_cb(old_action, exception)
# Decorated functions
- def install(self, action, action_installed_cb, error_cb):
+ def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None):
# Make a new copy of the action that we want to install,
# because translate() changes the action and we
# don't want to modify the caller's action representation
@@ -187,7 +187,9 @@ class ResourceTranslator(object):
# Send the new action to the probe manager
self._probe_manager.install(new_action, save_args(self.action_installed, action_installed_cb),
- save_args(self.action_install_error, error_cb, new_action))
+ save_args(self.action_install_error, error_cb, new_action),
+ is_editing=is_editing,
+ editing_cb=editing_cb)
def update(self, action_address, newaction):
translated_new_action = copy_module.deepcopy(newaction)
diff --git a/tutorius/viewer.py b/tutorius/viewer.py
index 56428e1..8041162 100644
--- a/tutorius/viewer.py
+++ b/tutorius/viewer.py
@@ -65,7 +65,8 @@ class Viewer(object):
self.drag_pos = None
self.selection = set()
- self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.win = gtk.Window(gtk.WINDOW_POPUP)
+ self.win.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY)
self.win.set_size_request(400, 200)
self.win.set_gravity(gtk.gdk.GRAVITY_SOUTH_WEST)
self.win.show()