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/creator.py') 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. -- cgit v0.9.1