From 825143f4e633c4025ba39307760a3924769f2f02 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Tue, 24 Nov 2009 00:30:17 +0000 Subject: creator+probes, not entirely working yet --- (limited to 'tutorius') diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index 0c79690..7d1b91a 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -10,6 +10,7 @@ import cPickle as pickle from . import addon +from . import properties from .services import ObjectStore from .properties import TPropContainer @@ -113,11 +114,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)) @@ -130,17 +132,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] @@ -148,26 +154,67 @@ 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 + from .creator import WidgetSelector, SignalInputDialog, TextInputDialog + + 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) + if isinstance(prop, properties.TUAMProperty): + selector = WidgetSelector(self._activity) + setattr(event, propname, selector.select()) + elif isinstance(prop, properties.TEventType): + try: + dlg = SignalInputDialog(self._activity, + text="Mandatory property", + field=propname, + addr=event.object_id) + setattr(event, propname, dlg.pop()) + except AttributeError: + pass + elif isinstance(prop, properties.TStringProperty): + dlg = TextInputDialog(self._activity, + text="Mandatory property", + field=propname) + setattr(event, propname, dlg.pop()) + else: + raise NotImplementedError() + + 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 @@ -304,39 +351,49 @@ class ProbeProxy: def __clear_action(self, action): self._actions.pop(action, None) - def install(self, action, block=False): + def install(self, action, block=False, is_editing=False): """ Install an action on the TProbe's activity @param action Action to install @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor @return None """ - return remote_call(self._probe.install, (pickle.dumps(action),), - save_args(self.__update_action, action), - block=block) + return remote_call(self._probe.install, + (pickle.dumps(action), is_editing), + save_args(self.__update_action, action), + block=block) - def update(self, action, newaction, block=False): + def update(self, action, newaction, block=False, is_editing=False): """ Update an already installed action's properties and run it again @param action 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 """ #TODO review how to make this work well - if not action in self._actions: + if not action in self._actions.keys(): raise RuntimeWarning("Action not installed") #TODO Check error handling - return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block) + return remote_call(self._probe.update, + (self._actions[action], + pickle.dumps(newaction._props), + is_editing), + block=block) - def uninstall(self, action, block=False): + def uninstall(self, action, block=False, is_editing=False): """ Uninstall an installed action @param action Action to uninstall @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor """ - if action in self._actions: - remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block) + if action in self._actions.keys(): + remote_call(self._probe.uninstall, + (self._actions.pop(action), is_editing), + block=block) def __update_event(self, event, callback, address): LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) @@ -386,6 +443,16 @@ 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, callback, block=True): """ Register an event listener @@ -441,6 +508,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. @@ -455,6 +524,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") @@ -465,36 +536,52 @@ class ProbeManager(object): currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) - def install(self, action, block=False): + def install(self, action, block=False, is_editing=False): """ Install an action on the current activity @param action Action to install @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, block) + return self._first_proxy(self.currentActivity).install(action, block, is_editing) else: raise RuntimeWarning("No activity attached") - def update(self, action, newaction, block=False): + def update(self, action, newaction, block=False, is_editing=False): """ Update an already installed action's properties and run it again @param action 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, newaction, block) + return self._first_proxy(self.currentActivity).update(action, newaction, block, is_editing) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action, block=False): + def uninstall(self, action, block=False, is_editing=False): """ Uninstall an installed action @param action 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._probes[self.currentActivity].uninstall(action, block, 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, block) diff --git a/tutorius/creator.py b/tutorius/creator.py index fd9f1e8..e8182b0 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -27,27 +27,68 @@ 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 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. + """ + bus = dbus.SessionBus() + proxy = bus.get_object(BUS_NAME, BUS_PATH) + return proxy + +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): + 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 + + 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 +109,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 +125,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 +177,8 @@ class Creator(object): .get(action, None) if not action_obj: return False - action_obj.exit_editmode() + #action_obj.exit_editmode() + self._probe_mgr.uninstall(action_obj, is_editing=True) self._tutorial.delete_action(action) self._overview.win.queue_draw() return True @@ -169,7 +226,8 @@ class Creator(object): return for action in self._tutorial.get_action_dict(self._state).values(): - action.exit_editmode() + #action.exit_editmode() + self._probe_mgr.uninstall(action, is_editing=True) self._state = state_name state_actions = self._tutorial.get_action_dict(self._state).values() @@ -194,54 +252,20 @@ class Creator(object): """ # 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._probe_mgr.uninstall(action, is_editing=True) + #action.exit_editmode() 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() + #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() + self._probe_mgr.install(action, 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) + #action._drag._eventbox.connect_after( + # "button-release-event", self._action_refresh_cb, action) self._overview.win.queue_draw() def _add_event_cb(self, widget, path): @@ -271,7 +295,7 @@ class Creator(object): 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) + prop.widget_class.run_dialog(None, event, propname) event_filters = self._tutorial.get_transition_dict(self._state) @@ -296,17 +320,22 @@ 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() + self._probe_mgr.uninstall(action, is_editing=True) + #action.exit_editmode() + self._probe_mgr.install(action, is_editing=True) + #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) + #action._drag._eventbox.connect_after( + # "button-release-event", self._action_refresh_cb, action) self._propedit.action = action self._overview.win.queue_draw() @@ -319,11 +348,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() + #action.exit_editmode() + self._probe_mgr.uninstall(action, is_editing=True) if kwargs.get('force', False): dialog = gtk.MessageDialog( - parent=self._activity, + parent=None, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, @@ -335,14 +365,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,16 +394,11 @@ class Creator(object): vault.Vault.saveTutorial(self._tutorial, self._metadata) - - def launch(*args, **kwargs): - """ - Launch and attach a creator to the currently running activity. - """ - activity = ObjectStore().activity - if not hasattr(activity, "_creator"): - activity._creator = Creator(activity) + def launch(self, *args): + assert False, "REMOVE THIS CALL!!!" launch = staticmethod(launch) + class ToolBox(object): """ Palette window for edition tools, including the actions, states and @@ -396,10 +416,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 +517,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/properties.py b/tutorius/properties.py index aab6a0c..01cd2c0 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, \ @@ -58,6 +59,8 @@ class TPropContainer(object): self._props[attr_name] = propinstance.validate( copy(propinstance.default)) + self.__id = hash(uuid.uuid4()) + def __getattribute__(self, name): """ Process the 'fake' read of properties in the appropriate instance @@ -111,21 +114,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/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