From 0c3f127c86af818d260966d2292b199757087157 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Sat, 11 Jul 2009 21:39:46 +0000 Subject: repackage --- (limited to 'tutorius/creator.py') diff --git a/tutorius/creator.py b/tutorius/creator.py new file mode 100644 index 0000000..7455ecb --- /dev/null +++ b/tutorius/creator.py @@ -0,0 +1,436 @@ +""" +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: -- cgit v0.9.1