From c14688d67a82b7ec7746beda90da915c98600a3d Mon Sep 17 00:00:00 2001 From: erick Date: Sat, 05 Dec 2009 21:03:59 +0000 Subject: Merge branch 'frame_integration' into revamped_dragndrop Conflicts: tutorius/actions.py --- (limited to 'tutorius') diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index b384e6c..acba26f 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -20,16 +20,20 @@ import os import gobject -import dbus import dbus.service import cPickle as pickle +from functools import partial + +from jarabe.model.shell import get_model +from sugar.bundle.activitybundle import ActivityBundle from . import addon +from . import properties from .services import ObjectStore -from .properties import TPropContainer from .dbustools import save_args, ignore, logError +from .gtkutils import find_widget, raddr_lookup import copy """ @@ -129,11 +133,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)) @@ -146,17 +151,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, probe=self) + 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] @@ -164,26 +174,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 @@ -201,7 +232,7 @@ class TProbe(dbus.service.Object): def callback(*args): self.notify(eventfilter) - eventfilter.install_handlers(callback, activity=self._activity) + eventfilter.install_handlers(callback, activity=self._activity, probe=self) name = self._generate_event_reference(eventfilter) self._subscribedEvents[name] = eventfilter @@ -216,7 +247,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() @@ -237,6 +267,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 @@ -263,6 +308,108 @@ class TProbe(dbus.service.Object): return name + str(suffix) + # ------------------ Helper functions specific to a component -------------- + def find_widget(self, base, path, ignore_errors=True): + """ + Finds a widget from a base object. Symmetric with retrieve_path + + @param base the parent widget + @param path fqdn-style target object name + + @return widget found + """ + return find_widget(base, path, ignore_errors) + + def retrieve_path(self, widget): + """ + Retrieve the path to access a specific widget. + Symmetric with find_widget. + + @param widget the widget to find a path for + + @return path to the widget + """ + return raddr_lookup(widget) + +class FrameProbe(TProbe): + """ + Identical to the base probe except that helper functions are redefined + to handle the four windows that are part of the Frame. + """ + # ------------------ Helper functions specific to a component -------------- + def find_widget(self, base, path, ignore_errors=True): + """ + Finds a widget from a base object. Symmetric with retrieve_path + + format for the path for the frame should be: + + frame:/// + where panel: top | bottom | left | right + path: number[.number]* + + @param base the parent widget + @param path fqdn-style target object name + + @return widget found + """ + protocol, p = path.split("://") + assert protocol == "frame" + + window, object_id = p.split("/") + if window == "top": + return find_widget(base._top_panel, object_id, ignore_errors) + elif window == "bottom": + return find_widget(base._bottom_panel, object_id, ignore_errors) + elif window == "left": + return find_widget(base._left_panel, object_id, ignore_errors) + elif window == "right": + return find_widget(base._right_panel, object_id, ignore_errors) + else: + raise RuntimeWarning("Invalid frame panel: '%s'"%window) + + return find_widget(base, path, ignore_errors) + + def retrieve_path(self, widget): + """ + Retrieve the path to access a specific widget. + Symmetric with find_widget. + + format for the path for the frame should be: + + frame:/// + where panel: top | bottom | left | right + path: number[.number]* + + @param widget the widget to find a path for + + @return path to the widget + """ + name = [] + child = widget + parent = widget.parent + while parent: + name.append(str(parent.get_children().index(child))) + child = parent + parent = child.parent + + name.append("0") # root object itself + name.reverse() + + window = "" + if parent._position == gtk.POS_TOP: + window = "top" + elif parent._position == gtk.POS_BOTTOM: + window = "bottom" + elif parent._position == gtk.POS_LEFT: + window = "left" + elif parent._position == gtk.POS_RIGHT: + window = "right" + else: + raise RuntimeWarning("Invalid root panel in frame: %s"%str(parent)) + + return "frame://"+window+"/"+(".".join(name)) + + class ProbeProxy: """ ProbeProxy is a Proxy class for connecting to a remote TProbe. @@ -284,6 +431,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 @@ -292,6 +440,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)) @@ -312,33 +471,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 @@ -346,19 +519,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)) @@ -409,7 +583,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 @@ -419,8 +604,6 @@ class ProbeProxy: @return address identifier used for unsubscribing """ LOGGER.debug("ProbeProxy :: Registering event %s", str(hash(event))) - #if not block: - # raise RuntimeError("This function does not allow non-blocking mode yet") # TODO elavoie 2009-07-25 When we will allow for patterns both # for event types and sources, we will need to revise the lookup @@ -429,11 +612,10 @@ 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() - @param block Force a synchroneous dbus call if True @return None """ LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address)) @@ -445,18 +627,19 @@ class ProbeProxy: else: LOGGER.debug("ProbeProxy :: unsubscribe address %s failed : not registered", address) - def detach(self, block=False): + def detach(self): """ Detach the ProbeProxy from it's TProbe. All installed actions and 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 @@ -482,49 +665,78 @@ class ProbeManager(object): def setCurrentActivity(self, activity_id): if not activity_id in self._probes: - raise RuntimeError("Activity not attached") + raise RuntimeError("Activity not attached, id : %s"%activity_id) self._current_activity = activity_id def getCurrentActivity(self): + # TODO : Insert the correct call to remember the current activity, + # taking the views and frame into account + current_act = get_model().get_active_activity() + current_act_bundle = ActivityBundle(current_act.get_bundle_path()) + current_act_id = current_act_bundle.get_bundle_id() + self._current_activity = current_act_id return self._current_activity 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") @@ -601,8 +813,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 b24d14e..7811dfd 100644 --- a/tutorius/actions.py +++ b/tutorius/actions.py @@ -28,6 +28,12 @@ from .properties import * from .constants import * +import pickle + +import logging + +LOGGER = logging.getLogger("actions") + class DragWrapper(object): """Wrapper to allow gtk widgets to be dragged around""" @@ -35,12 +41,13 @@ class DragWrapper(object): LOGGER = logging.getLogger("sugar.tutorius.actions.DragWrapper") - 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 @@ -51,6 +58,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 _drag_begin(self, widget, drag_context, *args): @@ -88,11 +96,13 @@ class DragWrapper(object): self.position = (int(xparent-xrel), int(yparent-yrel)) self.LOGGER.debug("%s drag-end pos: (%s,%s)"%(str(widget),self.position)) + LOGGER.debug("DragWrapper :: Sending update notification...") + self.update_action_cb('position', self.position) + self._widget.parent.move(self._eventbox, *self.position) self._widget.parent.move(self._widget, *self.position) self._widget.parent.queue_draw() - def set_draggable(self, value): """Setter for the draggable property""" if bool(value) ^ bool(self._drag_on): @@ -150,6 +160,9 @@ class Action(TPropContainer): TPropContainer.__init__(self) self.position = (0,0) self._drag = None + # The callback that will be triggered when the action is requested + # to notify all its changes + self._properties_updated_cb = None def do(self, **kwargs): """ @@ -163,6 +176,32 @@ class Action(TPropContainer): """ pass #Should raise NotImplemented? + + def set_notification_cb(self, notif_cb): + LOGGER.debug("Action :: Setting notification callback for creator...") + self._properties_updated_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))) + # Set the property itself - this will modify the diff dict and we will + # be able to notify the owner with the new 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, **kwargs): """ Enters edit mode. The action should display itself in some way, @@ -182,7 +221,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/constants.py b/tutorius/constants.py new file mode 100644 index 0000000..377b4a5 --- /dev/null +++ b/tutorius/constants.py @@ -0,0 +1,2 @@ +TARGET_TYPE_WIDGET = 81 +WIDGET_ID = "widget" diff --git a/tutorius/creator.py b/tutorius/creator.py index 68c5fa6..50017dc 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -21,83 +21,145 @@ 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. + """ + return Creator._instance + +def get_creator_proxy(): + """ + Returns a Creator dbus proxy for inter-process events. + """ + bus = SessionBus() + proxy = bus.get_object(BUS_NAME, BUS_PATH) + return proxy -class Creator(object): +class Creator(Object): """ - Class acting as a bridge between the creator, serialization and core - classes. This contains most of the UI part of the editor. + Class acting as a controller for the tutorial edition. """ - def __init__(self, activity, tutorial=None): + + _instance = None + + def __init__(self, probe_manager): + """ + Creates the instance of the creator. It is assumed this will be called + only once, by the Service. + + @param probe_manager The Probe Manager """ - Instanciate a tutorial creator for the activity. + bus_name = BusName(BUS_NAME, bus=SessionBus()) + Object.__init__(self, bus_name, BUS_PATH) - @param activity to bind the creator to - @param tutorial an existing tutorial to edit, or None to create one + self.tuto = None + self.is_authoring = False + if Creator._instance: + raise RuntimeError("Creator was already instanciated") + Creator._instance = self + self._probe_mgr = probe_manager + self._installed_actions = list() + + def start_authoring(self, tutorial=None): + """ + Start authoring a tutorial. + + @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) - - dlg_width = 300 - dlg_height = 70 - sw = gtk.gdk.screen_width() - sh = gtk.gdk.screen_height() + frame = jarabe.frame.get_view() - self._propedit = ToolBox(self._activity) + self._propedit = ToolBox(None) self._propedit.tree.signal_autoconnect({ - 'on_quit_clicked': self._cleanup_cb, + '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) - + 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() @@ -120,7 +182,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 +196,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 +209,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 transitions will + shift the current transition 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,22 +319,32 @@ class Creator(object): self.set_insertion_point(new_state) + def properties_changed(self, action): + LOGGER.debug("Creator :: properties_changed for action at address %s to %s"%(action.address)) + address = action.address + self._probe_mgr.update(address, + action, + is_editing=True) + 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) + # TODO : replace with update + 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() - def _cleanup_cb(self, *args, **kwargs): + def cleanup_cb(self, *args, **kwargs): """ Quit editing and cleanup interface artifacts. @@ -275,61 +352,117 @@ 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) - if kwargs.get('force', False): + # 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 not kwargs.get('force', False): + # TODO : Move the dialog in the middle of the screen 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 +470,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 +499,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 +555,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 +573,7 @@ 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) # 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/editor.py b/tutorius/editor.py index 9d2effe..7f2cc8e 100644 --- a/tutorius/editor.py +++ b/tutorius/editor.py @@ -23,9 +23,11 @@ import gobject #import gconf from gettext import gettext as _ +from sugar.graphics.window import Window from .gtkutils import register_signals_numbered, get_children + class WidgetIdentifier(gtk.Window): """ Tool that allows identifying widgets. @@ -126,12 +128,26 @@ class WidgetIdentifier(gtk.Window): typecol = gtk.TreeViewColumn(_("Widget"), typerendr, text=1, background=1, foreground=1) explorer.append_column(typecol) - self.__populate_treestore( - tree, #tree - tree.append(None, ["0",self._activity.get_name()]), #parent - self._activity, #widget - "0" #path - ) + + if isinstance(self._activity, Window): + self.__populate_treestore( + tree, #tree + tree.append(None, ["0",str(self._activity)]), #parent + self._activity, #widget + "0" #path + ) + else: + # Assume it is the frame + for win in [self._activity._left_panel,\ + self._activity._right_panel,\ + self._activity._top_panel,\ + self._activity._bottom_panel]: + self.__populate_treestore( + tree, #tree + tree.append(None, ["0",str(self._activity)]), #parent + win, #widget + "0" #path + ) explorer.set_expander_column(typecol) @@ -158,13 +174,25 @@ class WidgetIdentifier(gtk.Window): typecol2 = gtk.TreeViewColumn(_("Widget"), typerendr2, text=1, background=1, foreground=1) explorer2.append_column(typecol2) - self.__populate_gobject_treestore( - tree2, #tree - tree2.append(None, ["activity",self._activity.get_name()]), #parent - self._activity, #widget - "activity" #path - ) - + if isinstance(self._activity, Window): + self.__populate_gobject_treestore( + tree2, #tree + tree2.append(None, ["activity",str(self._activity)]), #parent + self._activity, #widget + "activity" #path + ) + else: + # Assume it is the frame + for win in [self._activity._left_panel,\ + self._activity._right_panel,\ + self._activity._top_panel,\ + self._activity._bottom_panel]: + self.__populate_gobject_treestore( + tree2, #tree + tree2.append(None, ["activity",str(self._activity)]), #parent + win, #widget + "activity" #path + ) explorer2.set_expander_column(typecol2) swd3 = gtk.ScrolledWindow() diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py index fcb6974..9cd840b 100644 --- a/tutorius/overlayer.py +++ b/tutorius/overlayer.py @@ -157,8 +157,8 @@ class Overlayer(gtk.Layout): # Since widget is laid out in a Layout box, the Layout will honor the # requested size. Using size_allocate could make a nasty nested loop in # some cases. - self._overlayed.set_size_request(allocation.width, allocation.height) - + if self._overlayed: + self._overlayed.set_size_request(allocation.width, allocation.height) class FrameOverlayer(gtk.Window): def __init__(self): diff --git a/tutorius/properties.py b/tutorius/properties.py index a462782..07b1645 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) @@ -123,26 +138,32 @@ class TPropContainer(object): """ Return a deep copy of the dictionary of properties from that object. """ - return deepcopy(self._props) + return deepcopy(self._props) - # 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]))))) - - def __eq__(self, e2): - return isinstance(e2, type(self)) and self._props == e2._props + """ + Deprecated. + + Property containers should not be used as keys inside dictionary for + the time being. Since containers are mutable, we should definitely + use the addressing mechanism to refer to the containers. + """ + # 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 # 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']) + + def __eq__(self, e2): + return (isinstance(e2, type(self)) and self._props == e2._props) class TutoriusProperty(object): """ diff --git a/tutorius/service.py b/tutorius/service.py index 11a94a5..1564339 100644 --- a/tutorius/service.py +++ b/tutorius/service.py @@ -3,6 +3,7 @@ import dbus from .engine import Engine from .dbustools import remote_call from .TProbe import ProbeManager +from .creator import Creator import logging LOGGER = logging.getLogger("sugar.tutorius.service") @@ -24,6 +25,8 @@ class Service(dbus.service.Object): self._probeMgr = ProbeManager() + Creator(self._probeMgr) + def start(self): """ Start the service itself """ diff --git a/tutorius/store.py b/tutorius/store.py index 565295d..69e74af 100644 --- a/tutorius/store.py +++ b/tutorius/store.py @@ -220,7 +220,7 @@ class StoreProxy(object): installnode = xml.getElementsByTagName("install")[0] installurl = installnode.firstChild.nodeValue - fp = urllib.urlopen(installurl) + fp = urllib2.urlopen(installurl) return fp 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() -- cgit v0.9.1