""" 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.gdk import gobject from gettext import gettext as T from sugar.graphics.toolbutton import ToolButton from sugar.tutorius import overlayer, gtkutils, actions, bundler, properties, addon from sugar.tutorius import filters from sugar.tutorius.services import ObjectStore from sugar.tutorius.linear_creator import LinearCreator from sugar.tutorius.core import Tutorial 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. """ def __init__(self, activity, tutorial=None): """ Instanciate a tutorial creator for the activity. @param activity to bind the creator to @param tutorial an existing tutorial to edit, or None to create one """ self._activity = activity if not tutorial: self._tutorial = LinearCreator() else: self._tutorial = tutorial self._action_panel = None self._current_filter = None self._intro_mask = None self._intro_handle = None self._state_bubble = overlayer.TextBubble(self._tutorial.state_name) allocation = self._activity.get_allocation() self._width = allocation.width self._height = allocation.height self._selected_widget = None self._eventmenu = None self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5)) self._activity._overlayer.put(self._hlmask, 0, 0) self._activity._overlayer.put(self._state_bubble, self._width/2-self._state_bubble.allocation.width/2, 0) dlg_width = 300 dlg_height = 70 sw = gtk.gdk.screen_width() sh = gtk.gdk.screen_height() self._tooldialog = gtk.Window() self._tooldialog.set_title("Tutorius tools") self._tooldialog.set_transient_for(self._activity) self._tooldialog.set_decorated(True) self._tooldialog.set_resizable(False) self._tooldialog.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) self._tooldialog.set_destroy_with_parent(True) self._tooldialog.set_deletable(False) self._tooldialog.set_size_request(dlg_width, dlg_height) toolbar = gtk.Toolbar() for tool in addon.list_addons(): meta = addon.get_addon_meta(tool) toolitem = ToolButton(meta['icon']) toolitem.set_tooltip(meta['display_name']) toolitem.connect("clicked", self._add_action_cb, tool) toolbar.insert(toolitem, -1) toolitem = ToolButton("go-next") toolitem.connect("clicked", self._add_step_cb) toolitem.set_tooltip("Add Step") toolbar.insert(toolitem, -1) toolitem = ToolButton("stop") toolitem.connect("clicked", self._cleanup_cb) toolitem.set_tooltip("End Tutorial") toolbar.insert(toolitem, -1) self._tooldialog.add(toolbar) self._tooldialog.show_all() # simpoir: I suspect the realized widget is a tiny bit larger than # it should be, thus the -10. self._tooldialog.move(sw-10-dlg_width, sh-dlg_height) self._propedit = EditToolBox(self._activity) def _evfilt_cb(self, menuitem, event_name, *args): """ 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. """ self.introspecting = False eventfilter = addon.create('GtkWidgetEventFilter', next_state=None, object_id=self._selected_widget, event_name=event_name) # undo actions so they don't persist through step editing for action in self._tutorial.current_actions: action.exit_editmode() self._tutorial.event(eventfilter) self._state_bubble.label = self._tutorial.state_name 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 set_intropecting(self, value): """ Set whether creator is in UI introspection mode. Setting this will connect necessary handlers. @param value True to setup introspection handlers. """ if bool(value) ^ bool(self._intro_mask): if value: self._intro_mask = overlayer.Mask(catch_events=True) self._intro_handle = self._intro_mask.connect_after( "button-press-event", self._intro_cb) self._activity._overlayer.put(self._intro_mask, 0, 0) else: self._intro_mask.catch_events = False self._intro_mask.disconnect(self._intro_handle) self._intro_handle = None self._activity._overlayer.remove(self._intro_mask) self._intro_mask = None def get_introspecting(self): """ Whether creator is in UI introspection (catch all event) mode. @return True if introspection handlers are connected, or False if not. """ return bool(self._intro_mask) introspecting = property(fset=set_intropecting, fget=get_introspecting) def _add_action_cb(self, widget, actiontype): """Callback for the action creation toolbar tool""" action = addon.create(actiontype) if isinstance(action, actions.Action): action.enter_editmode() self._tutorial.action(action) # FIXME: replace following with event catching action._drag._eventbox.connect_after( "button-release-event", self._action_refresh_cb, action) else: addonname = type(action).__name__ meta = addon.get_addon_meta(addonname) had_introspect = False for propname in meta['mandatory_props']: prop = getattr(type(action), propname) if isinstance(prop, properties.TUAMProperty): had_introspect = True self.introspecting = True elif isinstance(prop, properties.TStringProperty): dlg = TextInputDialog(title="Mandatory property", field=propname) setattr(action, propname, dlg.pop()) else: raise NotImplementedError() # FIXME: hack to reuse previous introspection code if not had_introspect: self._tutorial.event(action) 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._propedit.action = action def _add_step_cb(self, widget): """Callback for the "add step" tool""" self.introspecting = True def _cleanup_cb(self, *args): """ Quit editing and cleanup interface artifacts. """ self.introspecting = False eventfilter = filters.EventFilter(None) # undo actions so they don't persist through step editing for action in self._tutorial.current_actions: action.exit_editmode() self._tutorial.event(eventfilter) dlg = TextInputDialog(text=T("Enter a tutorial title."), field=T("Title")) tutorialName = "" while not tutorialName: tutorialName = dlg.pop() dlg.destroy() # prepare tutorial for serialization tuto = Tutorial(tutorialName, self._tutorial.fsm) bundle = bundler.TutorialBundler() bundle.write_metadata_file(tuto) bundle.write_fsm(self._tutorial.fsm) # remove UI remains self._hlmask.covered = None self._activity._overlayer.remove(self._hlmask) self._activity._overlayer.remove(self._state_bubble) self._hlmask.destroy() self._hlmask = None self._tooldialog.destroy() self._propedit.destroy() self._activity.queue_draw() del self._activity._creator 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) launch = staticmethod(launch) class EditToolBox(gtk.Window): """Helper toolbox class for managing action properties""" def __init__(self, parent, action=None): """ Create the property edition toolbox and display it. @param parent the parent window of this toolbox, usually an activity @param action the action to introspect/edit """ gtk.Window.__init__(self) self._action = None self.__parent = parent # private avoid gtk clash self.set_title("Action Properties") self.set_transient_for(parent) self.set_decorated(True) self.set_resizable(False) self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) self.set_destroy_with_parent(True) self.set_deletable(False) self.set_size_request(200, 400) self._vbox = gtk.VBox() self.add(self._vbox) propwin = gtk.ScrolledWindow() propwin.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC propwin.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC self._vbox.pack_start(propwin) self._propbox = gtk.VBox(spacing=10) propwin.add(self._propbox) self.action = action sw = gtk.gdk.screen_width() sh = gtk.gdk.screen_height() self.show_all() self.move(sw-10-200, (sh-400)/2) def refresh(self): """Refresh property values from the selected action.""" if self._action is None: return props = self._action._props.keys() for propnum in xrange(len(props)): row = self._propbox.get_children()[propnum] propname = props[propnum] prop = getattr(type(self._action), propname) propval = getattr(self._action, propname) if isinstance(prop, properties.TStringProperty): propwdg = row.get_children()[1] propwdg.get_buffer().set_text(propval) elif isinstance(prop, properties.TIntProperty): propwdg = row.get_children()[1] propwdg.set_value(propval) elif isinstance(prop, properties.TArrayProperty): propwdg = row.get_children()[1] for i in xrange(len(propval)): entry = propwdg.get_children()[i] entry.set_text(str(propval[i])) else: propwdg = row.get_children()[1] propwdg.set_text(str(propval)) def set_action(self, action): """Setter for the action property.""" if self._action is action: self.refresh() return parent = self._propbox.get_parent() parent.remove(self._propbox) self._propbox = gtk.VBox(spacing=10) parent.add(self._propbox) self._action = action if action is None: return for propname in action._props.keys(): row = gtk.HBox() row.pack_start(gtk.Label(T(propname)), False, False, 10) prop = getattr(type(action), propname) propval = getattr(action, propname) if isinstance(prop, properties.TStringProperty): propwdg = gtk.TextView() propwdg.get_buffer().set_text(propval) propwdg.connect_after("focus-out-event", \ self._str_prop_changed, action, propname) elif isinstance(prop, properties.TIntProperty): adjustment = gtk.Adjustment(value=propval, lower=prop.lower_limit.limit, upper=prop.upper_limit.limit, step_incr=1) propwdg = gtk.SpinButton(adjustment=adjustment) propwdg.connect_after("focus-out-event", \ self._int_prop_changed, action, prop) elif isinstance(prop, properties.TArrayProperty): propwdg = gtk.HBox() for i in xrange(len(propval)): entry = gtk.Entry() propwdg.pack_start(entry) entry.connect_after("focus-out-event", \ self._list_prop_changed, action, propname, i) else: propwdg = gtk.Entry() propwdg.set_text(str(propval)) row.pack_end(propwdg) self._propbox.pack_start(row, expand=False) self._vbox.show_all() self.refresh() 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 _list_prop_changed(self, widget, evt, action, propname, idx): try: getattr(action, propname)[idx] = int(widget.get_text()) except ValueError: widget.set_text(str(getattr(action, propname)[idx])) self.__parent._creator._action_refresh_cb(None, None, action) def _str_prop_changed(self, widget, evt, action, propname): buf = widget.get_buffer() setattr(action, propname, buf.get_text(buf.get_start_iter(), buf.get_end_iter())) self.__parent._creator._action_refresh_cb(None, None, action) def _int_prop_changed(self, widget, evt, action, prop): setattr(action, propname, widget.get_value_as_int()) self.__parent._creator._action_refresh_cb(None, None, action) class TextInputDialog(gtk.MessageDialog): def __init__(self, text, field): gtk.MessageDialog.__init__(self, None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK, None) self.set_markup(text) self.entry = gtk.Entry() self.entry.connect("activate", self._dialog_done_cb, gtk.RESPONSE_OK) hbox = gtk.HBox() lbl = gtk.Label(field) hbox.pack_start(lbl, False) hbox.pack_end(self.entry) self.vbox.pack_end(hbox, True, True) self.show_all() def pop(self): self.run() self.hide() text = self.entry.get_text() return text def _dialog_done_cb(self, entry, response): self.response(response) # vim:set ts=4 sts=4 sw=4 et: