From 50ec9ae6ebc7ecc088b5e0a6b79688a717271955 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Tue, 01 Dec 2009 05:57:04 +0000 Subject: creator with probe integration. updating existing objects doesn't work yet --- (limited to 'tutorius') diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index 4e458a0..5b5cf8d 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -20,14 +20,12 @@ import os import gobject -import dbus import dbus.service import cPickle as pickle from . import addon from .services import ObjectStore -from .properties import TPropContainer from .dbustools import save_args, ignore, logError import copy @@ -127,11 +125,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 +143,21 @@ 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() + 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 +165,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 @@ -318,45 +342,47 @@ class ProbeProxy: def __clear_action(self, address): self._actions.pop(address, None) - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False): """ Install an action on the TProbe's activity @param action Action to install @param action_installed_cb The callback function to call once the action is installed @param error_cb The callback function to call when an error happens + @param is_editing whether this action comes from the editor @return None """ - self._probe.install(pickle.dumps(action), - reply_handler=save_args(self.__update_action, action, action_installed_cb), + self._probe.install(pickle.dumps(action), is_editing, + reply_handler=save_args(self.__update_action, action, action_installed_cb), error_handler=save_args(error_cb, action)) - def update(self, action_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 if not action_address in self._actions.keys(): raise RuntimeWarning("Action not installed") #TODO Check error handling - return self._probe.update(action_address, pickle.dumps(newaction._props), + 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 +433,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 @@ -464,6 +501,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 +517,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") @@ -488,41 +529,61 @@ 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): """ Install an action on the current activity @param action Action to install @param action_installed_cb The callback to call once the action is installed @param error_cb The callback that will be called if there is an error during installation @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).install(action, 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) 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, 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).uninstall(action_address) + return self._first_proxy(self.currentActivity).create_event(addon_name) else: raise RuntimeWarning("No activity attached") diff --git a/tutorius/creator.py b/tutorius/creator.py index fd9f1e8..54e2912 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -27,27 +27,71 @@ from gettext import gettext as T import uuid import os -from sugar.graphics import icon +from sugar.graphics import icon, style +import jarabe.frame from . import overlayer, gtkutils, vault, addon from .services import ObjectStore from .tutorial import Tutorial from . import viewer from .propwidgets import TextInputDialog +from . import TProbe -class Creator(object): +from functools import partial + +from dbus import SessionBus +from dbus.service import method, Object, BusName + +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 + +def get_creator_proxy(): + """ + Returns a Creator dbus proxy for inter-process events. """ - Class acting as a bridge between the creator, serialization and core - classes. This contains most of the UI part of the editor. + bus = dbus.SessionBus() + proxy = bus.get_object(BUS_NAME, BUS_PATH) + return proxy + +class Creator(Object): """ - def __init__(self, activity, tutorial=None): + 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() @@ -68,21 +112,15 @@ class Creator(object): 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() - 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, @@ -90,18 +128,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 @@ -121,7 +180,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 @@ -168,80 +228,43 @@ class Creator(object): or state_name == self._tutorial.END: return - for action in self._tutorial.get_action_dict(self._state).values(): - action.exit_editmode() + for action in self._installed_actions: + self._probe_mgr.uninstall(action.address, + is_editing=True) + self._installed_actions = [] self._state = state_name state_actions = self._tutorial.get_action_dict(self._state).values() + for action in state_actions: - action.enter_editmode() - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True) if state_actions: + # I'm really lazy right now and to keep things simple I simply + # always select the first action when + # we change state. we should really select the clicked block + # in the overview instead. FIXME self._propedit.action = state_actions[0] else: self._propedit.action = None self._overview.win.queue_draw() - - def _evfilt_cb(self, menuitem, event): - """ - This will get called once the user has selected a menu item from the - event filter popup menu. This should add the correct event filter - to the FSM and increment states. - """ - # undo actions so they don't persist through step editing - for action in self._state.get_action_list(): - 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] 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) 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): @@ -266,12 +289,7 @@ class Creator(object): """ event_type = self._propedit.events_list[path][ToolBox.ICON_NAME] - event = addon.create(event_type) - addonname = type(event).__name__ - meta = addon.get_addon_meta(addonname) - for propname in meta['mandatory_props']: - prop = getattr(type(event), propname) - prop.widget_class.run_dialog(self._activity, event, propname) + event = self._probe_mgr.create_event(event_type) event_filters = self._tutorial.get_transition_dict(self._state) @@ -296,17 +314,21 @@ class Creator(object): self.set_insertion_point(new_state) + def properties_changed(self, action, properties): + 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) self._propedit.action = action self._overview.win.queue_draw() @@ -319,11 +341,12 @@ class Creator(object): """ # undo actions so they don't persist through step editing for action in self._tutorial.get_action_dict(self._state).values(): - action.exit_editmode() + self._probe_mgr.uninstall(action.address, + is_editing=True) 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, @@ -335,14 +358,9 @@ class Creator(object): 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): """ @@ -369,15 +387,27 @@ class Creator(object): 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, exception): + """ + This is a callback intented to be use to receive exceptions on remote + DBUS calls. + @param exception: the exception thrown by the remote process + """ + pass + class ToolBox(object): """ @@ -396,10 +426,13 @@ class ToolBox(object): '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) @@ -494,6 +527,7 @@ class ToolBox(object): 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. # To preserve layout, this call reformat text to fit in small space under an 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