""" This package contains UI classes related to tutorial authoring. This includes visual display of tools to edit and create tutorials from within the activity itself. """ # Copyright (C) 2009, Tutorius.org # Copyright (C) 2009, Simon Poirier # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 1 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # 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.glade import gobject from gettext import gettext as T import uuid import os 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 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 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 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. """ 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._selected_widget = None self._eventmenu = None self.tuto = None self._guid = None self.metadata = None frame = jarabe.frame.get_view() self._propedit = ToolBox(None) self._propedit.tree.signal_autoconnect({ '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\ -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(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(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 evts = state.get_event_filter_list() state.clear_event_filters() for evt, next_state in evts: state.add_event_filter(evt, self._transitions[evt]) def delete_action(self, action): """ Removes the first instance of specified action from the tutorial. @param action: the action name @returns: True if successful, otherwise False. """ action_obj = self._tutorial.get_action_dict(self._state)\ .get(action, None) if not action_obj: return False #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 def delete_state(self): """ Remove current state. Limitation: The last state cannot be removed, as it doesn't have any transitions to remove anyway. @returns: True if successful, otherwise False. """ 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 remove_state = self._state next_state = self._tutorial\ .get_following_states_dict(remove_state).keys()[0] self.set_insertion_point(next_state) 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): """ Set the tutorial modification point to the specified state. Actions of the state will enter the edit mode. New actions will be inserted to that state and new transisions will shift the current transision to the next state. @param state_name: the name of the state to use as insertion point """ # first is not modifiable, as the auto transition would make changes # pointless. The end state is also pointless to modify, as the tutorial # gets detached. if state_name == self._tutorial.INIT \ or state_name == self._tutorial.END: return for action in self._tutorial.get_action_dict(self._state).values(): #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() for action in state_actions: action.enter_editmode() action._drag._eventbox.connect_after( "button-release-event", self._action_refresh_cb, action) if state_actions: 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(): self._probe_mgr.uninstall(action, is_editing=True) #action.exit_editmode() self._propedit.action = None #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) 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) self._overview.win.queue_draw() def _add_event_cb(self, widget, path): """ 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(None, event, propname) event_filters = self._tutorial.get_transition_dict(self._state) # if not at the end of tutorial if event_filters: 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 # tutorial. new_state = self._tutorial.add_state() self._tutorial.add_transition(self._state, (event, new_state)) self._overview.win.queue_draw() 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. """ 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) self._propedit.action = action self._overview.win.queue_draw() def _cleanup_cb(self, *args, **kwargs): """ Quit editing and cleanup interface artifacts. @param force: force quitting without saving. """ # 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, is_editing=True) if kwargs.get('force', False): dialog = gtk.MessageDialog( parent=None, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, 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._propedit.destroy() self._overview.destroy() 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")) tutorial_name = "" while not tutorial_name: tutorial_name = dlg.pop() dlg.destroy() self._metadata = { vault.INI_GUID_PROPERTY: self._guid, vault.INI_NAME_PROPERTY: tutorial_name, vault.INI_VERSION_PROPERTY: '1', 'activities':{os.environ['SUGAR_BUNDLE_NAME']: os.environ['SUGAR_BUNDLE_VERSION'] }, } vault.Vault.saveTutorial(self._tutorial, self._metadata) 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 the editable property list of selected actions. """ ICON_LABEL = 0 ICON_IMAGE = 1 ICON_NAME = 2 ICON_TIP = 3 def __init__(self, parent): super(ToolBox, self).__init__() self.__parent = parent 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.events_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str) 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) iconfile = gtk.Image() iconfile.set_from_file(icon.get_icon_file_name(meta['icon'])) img = iconfile.get_pixbuf() label = format_multiline(meta['display_name']) if meta['type'] == addon.TYPE_ACTION: self.actions_list.append( (label, img, toolname, meta['display_name']) ) else: self.events_list.append( (label, img, toolname, meta['display_name']) ) iconview_action = self.tree.get_widget('iconview1') iconview_action.set_model(self.actions_list) iconview_action.set_text_column(self.ICON_LABEL) iconview_action.set_pixbuf_column(self.ICON_IMAGE) iconview_action.set_tooltip_column(self.ICON_TIP) iconview_event = self.tree.get_widget('iconview2') iconview_event.set_model(self.events_list) iconview_event.set_text_column(self.ICON_LABEL) iconview_event.set_pixbuf_column(self.ICON_IMAGE) iconview_event.set_tooltip_column(self.ICON_TIP) self.window.show() def destroy(self): """ clean and free the toolbox """ self.window.destroy() def refresh_properties(self): """Refresh property values from the selected action.""" if self._action is None: return #Refresh the property editors for prop in self._propedits: prop.refresh_widget() def set_action(self, action): """Setter for the action property.""" if self._action is action: self.refresh_properties() return #Clear the prop box for old_prop in self._propbox.get_children(): self._propbox.remove(old_prop) self._propedits = [] self._action = action if action is None: return for propname in action._props.keys(): row = gtk.HBox() #Label row.pack_start(gtk.Label(T(propname)), False, False, 10) #Value field prop = getattr(type(action), propname) propedit = prop.widget_class(self.__parent, action, propname, self._refresh_action_cb) self._propedits.append(propedit) row.pack_end(propedit.widget) #Add row self._propbox.pack_start(row, expand=False) self._propbox.show_all() self.refresh_properties() def get_action(self): """Getter for the action property""" return self._action action = property(fset=set_action, fget=get_action, doc=\ "Action to be edited through introspection.") def _refresh_action_cb(self): if self._action is not None: 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 # icon. def format_multiline(text, length=10, lines=3, line_separator='\n'): """ Reformat a text to fit in a small space. @param length: maximum char per line @param lines: maximum number of lines """ words = text.split(' ') line = list() return_val = [] linelen = 0 for word in words: t_len = linelen+len(word) if t_len < length: line.append(word) linelen = t_len+1 # count space else: if len(return_val)+1 < lines: return_val.append(' '.join(line)) line = list() linelen = 0 line.append(word) else: return_val.append(' '.join(line+['...'])) return line_separator.join(return_val) return_val.append(' '.join(line)) return line_separator.join(return_val) # vim:set ts=4 sts=4 sw=4 et: