Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tutorius/creator.py
diff options
context:
space:
mode:
authorSimon Poirier <simpoir@gmail.com>2009-07-11 21:39:46 (GMT)
committer Simon Poirier <simpoir@gmail.com>2009-07-11 22:00:30 (GMT)
commit0c3f127c86af818d260966d2292b199757087157 (patch)
tree62cf941aef5bde83641a17ec492e03d0ecb17386 /tutorius/creator.py
parent9fafb49af210e956d43d6a00106558d1a00d13df (diff)
repackage
Diffstat (limited to 'tutorius/creator.py')
-rw-r--r--tutorius/creator.py436
1 files changed, 436 insertions, 0 deletions
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 <simpoir@gmail.com>
+#
+#
+# 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: