From a188dac0527803edb46eabce04100f1c741a96f3 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Sun, 12 Jul 2009 21:34:20 +0000 Subject: repackage of tutorius using distutils --- (limited to 'tutorius') diff --git a/tutorius/__init__.py b/tutorius/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tutorius/__init__.py diff --git a/tutorius/actions.py b/tutorius/actions.py new file mode 100644 index 0000000..4269cd7 --- /dev/null +++ b/tutorius/actions.py @@ -0,0 +1,324 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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 +""" +This module defines Actions that can be done and undone on a state +""" +from gettext import gettext as _ + +from sugar.tutorius import gtkutils, addon +from dialog import TutoriusDialog +import overlayer +from sugar.tutorius.editor import WidgetIdentifier +from sugar.tutorius.services import ObjectStore +from sugar.tutorius.properties import * +from sugar.graphics import icon +import gtk.gdk + +class DragWrapper(object): + """Wrapper to allow gtk widgets to be dragged around""" + def __init__(self, widget, position, draggable=False): + """ + Creates a wrapper to allow gtk widgets to be mouse dragged, if the + parent container supports the move() method, like a gtk.Layout. + @param widget the widget to enhance with drag capability + @param position the widget's position. Will translate the widget if needed + @param draggable wether to enable the drag functionality now + """ + self._widget = widget + self._eventbox = None + self._drag_on = False # whether dragging is enabled + self._rel_pos = (0,0) # mouse pos relative to widget + self._handles = [] # event handlers + self._dragging = False # whether a drag is in progress + self.position = position # position of the widget + + self.draggable = draggable + + def _pressed_cb(self, widget, evt): + """Callback for start of drag event""" + self._eventbox.grab_add() + self._dragging = True + self._rel_pos = evt.get_coords() + + def _moved_cb(self, widget, evt): + """Callback for mouse drag events""" + if not self._dragging: + return + + # Focus on a widget before dragging another would + # create addititonal move event, making the widget jump unexpectedly. + # Solution found was to process those focus events before dragging. + if gtk.events_pending(): + return + + xrel, yrel = self._rel_pos + xparent, yparent = evt.get_coords() + xparent, yparent = widget.translate_coordinates(widget.parent, + xparent, yparent) + self.position = (xparent-xrel, yparent-yrel) + self._widget.parent.move(self._eventbox, *self.position) + self._widget.parent.move(self._widget, *self.position) + self._widget.parent.queue_draw() + + def _released_cb(self, *args): + """Callback for end of drag (mouse release).""" + self._eventbox.grab_remove() + self._dragging = False + + def _drag_end(self, *args): + """Callback for end of drag (stolen focus).""" + self._dragging = False + + def set_draggable(self, value): + """Setter for the draggable property""" + if bool(value) ^ bool(self._drag_on): + if value: + self._eventbox = gtk.EventBox() + self._eventbox.show() + self._eventbox.set_visible_window(False) + size = self._widget.size_request() + self._eventbox.set_size_request(*size) + self._widget.parent.put(self._eventbox, *self.position) + self._handles.append(self._eventbox.connect( + "button-press-event", self._pressed_cb)) + self._handles.append(self._eventbox.connect( + "button-release-event", self._released_cb)) + self._handles.append(self._eventbox.connect( + "motion-notify-event", self._moved_cb)) + self._handles.append(self._eventbox.connect( + "grab-broken-event", self._drag_end)) + else: + while len(self._handles): + handle = self._handles.pop() + self._eventbox.disconnect(handle) + self._eventbox.parent.remove(self._eventbox) + self._eventbox.destroy() + self._eventbox = None + self._drag_on = value + + def get_draggable(self): + """Getter for the draggable property""" + return self._drag_on + + draggable = property(fset=set_draggable, fget=get_draggable, \ + doc="Property to enable the draggable behaviour of the widget") + + def set_widget(self, widget): + """Setter for the widget property""" + if self._dragging or self._drag_on: + raise Exception("Can't change widget while dragging is enabled.") + + assert hasattr(widget, "parent"), "wrapped widget should have a parent" + parent = widget.parent + assert hasattr(parent, "move"), "container of widget need move method" + self._widget = widget + + def get_widget(self): + """Getter for the widget property""" + return self._widget + + widget = property(fset=set_widget, fget=get_widget) + +class Action(TPropContainer): + """Base class for Actions""" + def __init__(self): + TPropContainer.__init__(self) + self.position = (0,0) + self._drag = None + + def do(self, **kwargs): + """ + Perform the action + """ + raise NotImplementedError("Not implemented") + + def undo(self): + """ + Revert anything the action has changed + """ + pass #Should raise NotImplemented? + + def enter_editmode(self, **kwargs): + """ + Enters edit mode. The action should display itself in some way, + without affecting the currently running application. The default is + a small box with the action icon. + """ + meta = addon.get_addon_meta(type(self).__name__) + + actionicon = icon.Icon(icon_name=meta['icon'], + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + # Eventbox create a visible window for the icon, so it clips correctly + self.__edit_img = gtk.EventBox() + self.__edit_img.set_visible_window(True) + self.__edit_img.add(actionicon) + + x, y = self.position + + ObjectStore().activity._overlayer.put(self.__edit_img, x, y) + self.__edit_img.show_all() + self._drag = DragWrapper(self.__edit_img, self.position, True) + + def exit_editmode(self, **kwargs): + x, y = self._drag.position + self.position = [int(x), int(y)] + self.__edit_img.destroy() + +class OnceWrapper(Action): + """ + Wraps a class to perform an action once only + + This ConcreteActions's do() method will only be called on the first do() + and the undo() will be callable after do() has been called + """ + + _action = TAddonProperty() + + def __init__(self, action): + Action.__init__(self) + self._called = False + self._need_undo = False + self._action = action + + def do(self): + """ + Do the action only on the first time + """ + if not self._called: + self._called = True + self._action.do() + self._need_undo = True + + def undo(self): + """ + Undo the action if it's been done + """ + if self._need_undo: + self._action.undo() + self._need_undo = False + +class WidgetIdentifyAction(Action): + def __init__(self): + Action.__init__(self) + self.activity = None + self._dialog = None + + def do(self): + os = ObjectStore() + if os.activity: + self.activity = os.activity + + self._dialog = WidgetIdentifier(self.activity) + self._dialog.show() + + + def undo(self): + if self._dialog: + self._dialog.destroy() + +class ChainAction(Action): + """Utility class to allow executing actions in a specific order""" + def __init__(self, *actions): + """ChainAction(action1, ... ) builds a chain of actions""" + Action.__init__(self) + self._actions = actions + + def do(self,**kwargs): + """do() each action in the chain""" + for act in self._actions: + act.do(**kwargs) + + def undo(self): + """undo() each action in the chain, starting with the last""" + for act in reversed(self._actions): + act.undo() + +class DisableWidgetAction(Action): + def __init__(self, target): + """Constructor + @param target target treeish + """ + Action.__init__(self) + self._target = target + self._widget = None + + def do(self): + """Action do""" + os = ObjectStore() + if os.activity: + self._widget = gtkutils.find_widget(os.activity, self._target) + if self._widget: + self._widget.set_sensitive(False) + + def undo(self): + """Action undo""" + if self._widget: + self._widget.set_sensitive(True) + + +class TypeTextAction(Action): + """ + Simulate a user typing text in a widget + Work on any widget that implements a insert_text method + + @param widget The treehish representation of the widget + @param text the text that is typed + """ + def __init__(self, widget, text): + Action.__init__(self) + + self._widget = widget + self._text = text + + def do(self, **kwargs): + """ + Type the text + """ + widget = gtkutils.find_widget(ObjectStore().activity, self._widget) + if hasattr(widget, "insert_text"): + widget.insert_text(self._text, -1) + + def undo(self): + """ + no undo + """ + pass + +class ClickAction(Action): + """ + Action that simulate a click on a widget + Work on any widget that implements a clicked() method + + @param widget The threehish representation of the widget + """ + def __init__(self, widget): + Action.__init__(self) + self._widget = widget + + def do(self): + """ + click the widget + """ + widget = gtkutils.find_widget(ObjectStore().activity, self._widget) + if hasattr(widget, "clicked"): + widget.clicked() + + def undo(self): + """ + No undo + """ + pass + diff --git a/tutorius/addon.py b/tutorius/addon.py new file mode 100644 index 0000000..51791d1 --- /dev/null +++ b/tutorius/addon.py @@ -0,0 +1,76 @@ +# 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 2 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 +""" +This module manages the loading and listing of tutorius addons. +Addons are modular actions and events that are package in such a way that they +can be autodetected and can integrate with Tutorius components (the editor) +without any configuration or explicit dependencies (python import). + +An action addon is expected to have a metadata dict such as this one: +__action__ = { + "name" : "HelloWorld", + "display_name" : "Hello World!", + "icon" : "hello", + "class" : HelloAction, + "mandatory_props" : ["text"], +} +""" + +import os +import re +import logging + +PREFIX = __name__+"s" +PATH = re.sub("addon\\.py[c]$", "", __file__)+"addons" + +_cache = None + +def _reload_addons(): + global _cache + _cache = {} + for addon in filter(lambda x: x.endswith("py"), os.listdir(PATH)): + mod = __import__(PREFIX+'.'+re.sub("\\.py$", "", addon), {}, {}, [""]) + if hasattr(mod, "__action__"): + _cache[mod.__action__['name']] = mod.__action__ + continue + if hasattr(mod, "__event__"): + _cache[mod.__event__['name']] = mod.__event__ + +def create(name, *args, **kwargs): + global _cache + if not _cache: + _reload_addons() + try: + return _cache[name]['class'](*args, **kwargs) + except KeyError: + logging.error("Addon not found for class '%s'", name) + return None + +def list_addons(): + global _cache + if not _cache: + _reload_addons() + return _cache.keys() + +def get_addon_meta(name): + global _cache + if not _cache: + _reload_addons() + return _cache[name] + +# vim:set ts=4 sts=4 sw=4 et: diff --git a/tutorius/bundler.py b/tutorius/bundler.py new file mode 100644 index 0000000..8e7fc3d --- /dev/null +++ b/tutorius/bundler.py @@ -0,0 +1,556 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Jean-Christophe Savard +# +# 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 2 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 + + +""" +This module contains all the data handling class of Tutorius +""" + +import logging +import os +import uuid +import xml.dom.minidom + +from sugar.tutorius import addon +from sugar.tutorius.core import Tutorial, State, FiniteStateMachine +from sugar.tutorius.filters import * +from sugar.tutorius.actions import * +from ConfigParser import SafeConfigParser + +# this is where user installed/generated tutorials will go +def _get_store_root(): + return os.path.join(os.getenv("HOME"),".sugar",os.getenv("SUGAR_PROFILE"),"tutorius","data") +# this is where activity bundled tutorials should be, under the activity bundle +def _get_bundle_root(): + return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data") + +INI_ACTIVITY_SECTION = "RELATED_ACTIVITIES" +INI_METADATA_SECTION = "GENERAL_METADATA" +INI_GUID_PROPERTY = "GUID" +INI_NAME_PROPERTY = "NAME" +INI_XML_FSM_PROPERTY = "FSM_FILENAME" +INI_FILENAME = "meta.ini" +TUTORIAL_FILENAME = "tutorial.xml" +NODE_COMPONENT = "Component" + +class TutorialStore(object): + + def list_available_tutorials(self, activity_name, activity_vers): + """ + Generate the list of all tutorials present on disk for a + given activity. + + @returns a map of tutorial {names : GUID}. + """ + # check both under the activity data and user installed folders + paths = [_get_store_root(), _get_bundle_root()] + + tutoGuidName = {} + + for repository in paths: + # (our) convention dictates that tutorial folders are named + # with their GUID (for unicity) but this is not enforced. + try: + for tuto in os.listdir(repository): + parser = SafeConfigParser() + parser.read(os.path.join(repository, tuto, INI_FILENAME)) + guid = parser.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) + name = parser.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) + activities = parser.options(INI_ACTIVITY_SECTION) + # enforce matching activity name AND version, as UI changes + # break tutorials. We may lower this requirement when the + # UAM gets less dependent on the widget order. + # Also note property names are always stored lowercase. + if activity_name.lower() in activities: + version = parser.get(INI_ACTIVITY_SECTION, activity_name) + if activity_vers == version: + tutoGuidName[guid] = name + except OSError: + # the repository may not exist. Continue scanning + pass + + return tutoGuidName + + def load_tutorial(self, Guid): + """ + Rebuilds a tutorial object from it's serialized state. + Common storing paths will be scanned. + + @param Guid the generic identifier of the tutorial + @returns a Tutorial object containing an FSM + """ + bundle = TutorialBundler(Guid) + bundle_path = bundle.get_tutorial_path() + config = SafeConfigParser() + config.read(os.path.join(bundle_path, INI_FILENAME)) + + serializer = XMLSerializer() + + name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) + fsm = serializer.load_fsm(Guid) + + tuto = Tutorial(name, fsm) + return tuto + + +class Serializer(object): + """ + Interface that provide serializing and deserializing of the FSM + used in the tutorials to/from disk. Must be inherited. + """ + + def save_fsm(self,fsm): + """ + Save fsm to disk. If a GUID parameter is provided, the existing GUID is + located in the .ini files in the store root and bundle root and + the corresponding FSM is/are overwritten. If the GUId is not found, an + exception occur. If no GUID is provided, FSM is written in a new file + in the store root. + """ + NotImplementedError + + def load_fsm(self): + """ + Load fsm from disk. + """ + NotImplementedError + +class XMLSerializer(Serializer): + """ + Class that provide serializing and deserializing of the FSM + used in the tutorials to/from a .xml file. Inherit from Serializer + """ + + def _create_state_dict_node(self, state_dict, doc): + """ + Create and return a xml Node from a State dictionnary. + """ + statesList = doc.createElement("States") + for state_name, state in state_dict.items(): + stateNode = doc.createElement("State") + statesList.appendChild(stateNode) + stateNode.setAttribute("Name", state_name) + actionsList = stateNode.appendChild(self._create_action_list_node(state.get_action_list(), doc)) + eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc)) + return statesList + + def _create_component_node(self, comp, doc): + """ + Takes a single component (action or eventfilter) and transforms it + into a xml node. + + @param comp A single component + @param doc The XML document root (used to create nodes only + @return A XML Node object with the component tag name + """ + compNode = doc.createElement(NODE_COMPONENT) + + # Write down just the name of the Action class as the Class + # property -- + compNode.setAttribute("Class",type(comp).__name__) + + # serialize all tutorius properties + for propname in comp.get_properties(): + propval = getattr(comp, propname) + if getattr(type(comp), propname).type == "addonlist": + for subval in propval: + compNode.appendChild(self._create_component_node(subval, doc)) + elif getattr(type(comp), propname).type == "addonlist": + compNode.appendChild(self._create_component_node(subval, doc)) + else: + # repr instead of str, as we want to be able to eval() it into a + # valid object. + compNode.setAttribute(propname, repr(propval)) + + return compNode + + def _create_action_list_node(self, action_list, doc): + """ + Create and return a xml Node from a Action list. + + @param action_list A list of actions + @param doc The XML document root (used to create new nodes only) + @return A XML Node object with the Actions tag name and a serie of + Action children + """ + actionsList = doc.createElement("Actions") + for action in action_list: + # Create the action node + actionNode = self._create_component_node(action, doc) + # Append it to the list + actionsList.appendChild(actionNode) + + return actionsList + + def _create_event_filters_node(self, event_filters, doc): + """ + Create and return a xml Node from a event filters. + """ + eventFiltersList = doc.createElement("EventFiltersList") + for event_f in event_filters: + eventFilterNode = self._create_component_node(event_f, doc) + eventFiltersList.appendChild(eventFilterNode) + + return eventFiltersList + + def save_fsm(self, fsm, xml_filename, path): + """ + Save fsm to disk, in the xml file specified by "xml_filename", in the + "path" folder. If the specified file doesn't exist, it will be created. + """ + self.doc = doc = xml.dom.minidom.Document() + fsm_element = doc.createElement("FSM") + doc.appendChild(fsm_element) + fsm_element.setAttribute("Name", fsm.name) + fsm_element.setAttribute("StartStateName", fsm.start_state_name) + statesDict = fsm_element.appendChild(self._create_state_dict_node(fsm._states, doc)) + + fsm_actions_node = self._create_action_list_node(fsm.actions, doc) + fsm_actions_node.tagName = "FSMActions" + actionsList = fsm_element.appendChild(fsm_actions_node) + + file_object = open(os.path.join(path, xml_filename), "w") + file_object.write(doc.toprettyxml()) + file_object.close() + + + def _find_tutorial_dir_with_guid(self, guid): + """ + Finds the tutorial with the associated GUID. If it is found, return + the path to the tutorial's directory. If it doesn't exist, raise an + IOError. + + A note : if there are two tutorials with this GUID in the folders, + they will both be inspected and the one with the highest version + number will be returned. If they have the same version number, the one + from the global store will be returned. + + @param guid The GUID of the tutorial that is to be loaded. + """ + # Attempt to find the tutorial's directory in the global directory + global_dir = os.path.join(_get_store_root(), guid) + # Then in the activty's bundle path + activity_dir = os.path.join(_get_bundle_root(), guid) + + # If they both exist + if os.path.isdir(global_dir) and os.path.isdir(activity_dir): + # Inspect both metadata files + global_meta = os.path.join(global_dir, "meta.ini") + activity_meta = os.path.join(activity_dir, "meta.ini") + + # Open both config files + global_parser = SafeConfigParser() + global_parser.read(global_meta) + + activity_parser = SafeConfigParser() + activity_parser.read(activity_meta) + + # Get the version number for each tutorial + global_version = global_parser.get(INI_METADATA_SECTION, "version") + activity_version = activity_parser.get(INI_METADATA_SECTION, "version") + + # If the global version is higher or equal, we'll take it + if global_version >= activity_version: + return global_dir + else: + return activity_dir + + # Do we just have the global directory? + if os.path.isdir(global_dir): + return global_dir + + # Or just the activity's bundle directory? + if os.path.isdir(activity_dir): + return activity_dir + + # Error : none of these directories contain the tutorial + raise IOError(2, "Neither the global nor the bundle directory contained the tutorial with GUID %s"%guid) + + def _load_xml_properties(self, properties_elem): + """ + Changes a list of properties into fully instanciated properties. + + @param properties_elem An XML element reprensenting a list of + properties + """ + return [] + + def _load_xml_event_filters(self, filters_elem): + """ + Loads up a list of Event Filters. + + @param filters_elem An XML Element representing a list of event filters + """ + reformed_event_filters_list = [] + event_filter_element_list = filters_elem.getElementsByTagName(NODE_COMPONENT) + new_event_filter = None + + for event_filter in event_filter_element_list: + new_event_filter = self._load_xml_component(event_filter) + + if new_event_filter is not None: + reformed_event_filters_list.append(new_event_filter) + + return reformed_event_filters_list + + def _load_xml_component(self, node): + """ + Loads a single addon component instance from an Xml node. + + @param node The component XML Node to transform + object + @return The addon component object of the correct type according to the XML + description + """ + new_action = addon.create(node.getAttribute("Class")) + if not new_action: + return None + + for attrib in node.attributes.keys(): + if attrib == "Class": continue + # security note: keep sandboxed + setattr(new_action, attrib, eval(node.getAttribute(attrib), {}, {})) + + # recreate complex attributes + for sub in node.childNodes: + name = getattr(new_action, sub.nodeName) + if name == "addon": + setattr(new_action, sub.getAttribute("Name"), self._load_xml_action(sub)) + + return new_action + + def _load_xml_actions(self, actions_elem): + """ + Transforms an Actions element into a list of instanciated Action. + + @param actions_elem An XML Element representing a list of Actions + """ + reformed_actions_list = [] + actions_element_list = actions_elem.getElementsByTagName(NODE_COMPONENT) + + for action in actions_element_list: + new_action = self._load_xml_component(action) + + reformed_actions_list.append(new_action) + + return reformed_actions_list + + def _load_xml_states(self, states_elem): + """ + Takes in a States element and fleshes out a complete list of State + objects. + + @param states_elem An XML Element that represents a list of States + """ + reformed_state_list = [] + # item(0) because there is always only one tag in the xml file + # so states_elem should always contain only one element + states_element_list = states_elem.item(0).getElementsByTagName("State") + + for state in states_element_list: + stateName = state.getAttribute("Name") + # Using item 0 in the list because there is always only one + # Actions and EventFilterList element per State node. + actions_list = self._load_xml_actions(state.getElementsByTagName("Actions")[0]) + event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList")[0]) + reformed_state_list.append(State(stateName, actions_list, event_filters_list)) + + return reformed_state_list + + def _load_xml_fsm(self, fsm_elem): + """ + Takes in an XML element representing an FSM and returns the fully + crafted FSM. + + @param fsm_elem The XML element that describes a FSM + """ + # Load the FSM's name and start state's name + fsm_name = fsm_elem.getAttribute("Name") + + fsm_start_state_name = None + try: + fsm_start_state_name = fsm_elem.getAttribute("StartStateName") + except: + pass + + fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name) + + # Load the states + states = self._load_xml_states(fsm_elem.getElementsByTagName("States")) + for state in states: + fsm.add_state(state) + + # Load the actions on this FSM + actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions")[0]) + for action in actions: + fsm.add_action(action) + + # Load the event filters + events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0]) + for event in events: + fsm.add_event_filter(event) + + return fsm + + + def load_fsm(self, guid): + """ + Load fsm from xml file whose .ini file guid match argument guid. + """ + # Fetch the directory (if any) + tutorial_dir = self._find_tutorial_dir_with_guid(guid) + + # Open the XML file + tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME) + + xml_dom = xml.dom.minidom.parse(tutorial_file) + + fsm_elem = xml_dom.getElementsByTagName("FSM")[0] + + return self._load_xml_fsm(fsm_elem) + + +class TutorialBundler(object): + """ + This class provide the various data handling methods useable by the tutorial + editor. + """ + + def __init__(self,generated_guid = None): + """ + Tutorial_bundler constructor. If a GUID is given in the parameter, the + Tutorial_bundler object will be associated with it. If no GUID is given, + a new GUID will be generated, + """ + + self.Guid = generated_guid or str(uuid.uuid1()) + + #Look for the file in the path if a uid is supplied + if generated_guid: + #General store + store_path = os.path.join(_get_store_root(), generated_guid, INI_FILENAME) + if os.path.isfile(store_path): + self.Path = os.path.dirname(store_path) + else: + #Bundle store + bundle_path = os.path.join(_get_bundle_root(), generated_guid, INI_FILENAME) + if os.path.isfile(bundle_path): + self.Path = os.path.dirname(bundle_path) + else: + raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid) + + else: + #Create the folder, any failure will go through to the caller for now + store_path = os.path.join(_get_store_root(), self.Guid) + os.makedirs(store_path) + self.Path = store_path + + def write_metadata_file(self, tutorial): + """ + Write metadata to the property file. + @param tutorial Tutorial for which to write metadata + """ + #Create the Config Object and populate it + cfg = SafeConfigParser() + cfg.add_section(INI_METADATA_SECTION) + cfg.set(INI_METADATA_SECTION, INI_GUID_PROPERTY, self.Guid) + cfg.set(INI_METADATA_SECTION, INI_NAME_PROPERTY, tutorial.name) + cfg.set(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY, TUTORIAL_FILENAME) + cfg.add_section(INI_ACTIVITY_SECTION) + cfg.set(INI_ACTIVITY_SECTION, os.environ['SUGAR_BUNDLE_NAME'], + os.environ['SUGAR_BUNDLE_VERSION']) + + #Write the ini file + cfg.write( file( os.path.join(self.Path, INI_FILENAME), 'w' ) ) + + def get_tutorial_path(self): + """ + Return the path of the .ini file associated with the guiven guid set in + the Guid property of the Tutorial_Bundler. If the guid is present in + more than one path, the store_root is given priority. + """ + + store_root = _get_store_root() + bundle_root = _get_bundle_root() + + config = SafeConfigParser() + path = None + + logging.debug("************ Path of store_root folder of activity : " \ + + store_root) + + # iterate in each GUID subfolder + for dir in os.listdir(store_root): + + # iterate for each .ini file in the store_root folder + + for file_name in os.listdir(os.path.join(store_root, dir)): + if file_name.endswith(".ini"): + logging.debug("******************* Found .ini file : " \ + + file_name) + config.read(os.path.join(store_root, dir, file_name)) + if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == self.Guid: + xml_filename = config.get(INI_METADATA_SECTION, + INI_XML_FSM_PROPERTY) + + path = os.path.join(store_root, dir) + return path + + logging.debug("************ Path of bundle_root folder of activity : " \ + + bundle_root) + + + # iterate in each GUID subfolder + for dir in os.listdir(bundle_root): + + # iterate for each .ini file in the bundle_root folder + for file_name in os.listdir(os.path.join(bundle_root, dir)): + if file_name.endswith(".ini"): + logging.debug("******************* Found .ini file : " \ + + file_name) + config.read(os.path.join(bundle_root, dir, file_name)) + if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == self.Guid: + path = os.path.join(bundle_root, self.Guid) + return path + + if path is None: + logging.debug("**************** Error : GUID not found") + raise KeyError + + def write_fsm(self, fsm): + + """ + Save fsm to disk. If a GUID parameter is provided, the existing GUID is + located in the .ini files in the store root and bundle root and + the corresponding FSM is/are created or overwritten. If the GUID is not + found, an exception occur. + """ + + config = SafeConfigParser() + + serializer = XMLSerializer() + path = os.path.join(self.Path, "meta.ini") + config.read(path) + xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) + serializer.save_fsm(fsm, xml_filename, self.Path) + + + def add_resources(self, typename, file): + """ + Add ressources to metadata. + """ + raise NotImplementedError("add_resources not implemented") diff --git a/tutorius/constraints.py b/tutorius/constraints.py new file mode 100644 index 0000000..36abdfb --- /dev/null +++ b/tutorius/constraints.py @@ -0,0 +1,207 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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 +""" +Constraints + +Defines a set of constraints with their related errors. These constraints are +made to be used inside TutoriusProperties in order to limit the values that +they might take. They can also be used to enforce a particular format or type +for some properties. +""" + +# For the File Constraint +import os + +class Constraint(): + """ + Basic block for defining constraints on a TutoriusProperty. Every class + inheriting from Constraint will have a validate function that will be + executed when the property's value is to be changed. + """ + def validate(self, value): + """ + This function receives the value that is proposed as a new value for + the property. It needs to raise an Error in the case where the value + does not respect this constraint. + """ + raise NotImplementedError("Unable to validate a base Constraint") + +class ValueConstraint(Constraint): + """ + A value constraint contains a _limit member that can be used in a children + class as a basic value. See UpperLimitConstraint for an exemple. + """ + def __init__(self, limit): + self.limit = limit + +class UpperLimitConstraintError(Exception): + pass + +class UpperLimitConstraint(ValueConstraint): + def validate(self, value): + """ + Evaluates whether the given value is smaller than the limit. + + @raise UpperLimitConstraintError When the value is strictly higher than + the limit. + """ + if self.limit is not None: + if self.limit >= value: + return + raise UpperLimitConstraintError() + return + +class LowerLimitConstraintError(Exception): + pass + +class LowerLimitConstraint(ValueConstraint): + def validate(self, value): + """ + If the value is lower than the limit, this function raises an error. + + @raise LowerLimitConstraintError When the value is strictly smaller + than the limit. + """ + if self.limit is not None: + if value >= self.limit: + return + raise LowerLimitConstraintError() + return + +class MaxSizeConstraintError(Exception): + pass + +class MaxSizeConstraint(ValueConstraint): + def validate(self, value): + """ + Evaluate whether a given object is smaller than the given size when + run through len(). Great for string, lists and the like. ;) + + @raise SizeConstraintError If the length of the value is strictly + bigger than the limit. + """ + if self.limit is not None: + if self.limit >= len(value): + return + raise MaxSizeConstraintError("Setter : trying to set value of length %d while limit is %d"%(len(value), self.limit)) + return + +class MinSizeConstraintError(Exception): + pass + +class MinSizeConstraint(ValueConstraint): + def validate(self, value): + """ + Evaluate whether a given object is smaller than the given size when + run through len(). Great for string, lists and the like. ;) + + @raise SizeConstraintError If the length of the value is strictly + bigger than the limit. + """ + if self.limit is not None: + if self.limit <= len(value): + return + raise MinSizeConstraintError("Setter : trying to set value of length %d while limit is %d"%(len(value), self.limit)) + return + +class ColorConstraintError(Exception): + pass + +class ColorArraySizeError(ColorConstraintError): + pass + +class ColorTypeError(ColorConstraintError): + pass + +class ColorValueError(ColorConstraintError): + pass + +class ColorConstraint(Constraint): + """ + Validates that the value is an array of size 3 with three numbers between + 0 and 255 (inclusively) in it. + + """ + def validate(self, value): + if len(value) != 3: + raise ColorArraySizeError("The value is not an array of size 3") + + if not (type(value[0]) == type(22) and type(value[1]) == type(22) and type(value[2]) == type(22)): + raise ColorTypeError("Not all the elements of the array are integers") + + if value[0] > 255 or value[0] <0: + raise ColorValueError("Red value is not between 0 and 255") + + if value[1] > 255 or value[1] <0: + raise ColorValueError("Green value is not between 0 and 255") + + if value[2] > 255 or value[2] <0: + raise ColorValueError("Blue value is not between 0 and 255") + + return + +class BooleanConstraintError(Exception): + pass + +class BooleanConstraint(Constraint): + """ + Validates that the value is either True or False. + """ + def validate(self, value): + if value == True or value == False: + return + raise BooleanConstraintError("Value is not True or False") + +class EnumConstraintError(Exception): + pass + +class EnumConstraint(Constraint): + """ + Validates that the value is part of a set of well-defined values. + """ + def __init__(self, accepted_values): + """ + Creates the constraint and stores the list of accepted values. + + @param correct_values A list that contains all the values that will + be declared as satisfying the constraint + """ + self._accepted_values = accepted_values + + def validate(self, value): + """ + Ensures that the value that is passed is part of the list of accepted + values. + """ + if not value in self._accepted_values: + raise EnumConstraintError("Value is not part of the enumeration") + return + +class FileConstraintError(Exception): + pass + +class FileConstraint(Constraint): + """ + Ensures that the string given corresponds to an existing file on disk. + """ + def validate(self, value): + # TODO : Decide on the architecture for file retrieval on disk + # Relative paths? From where? Support macros? + # + if not os.path.isfile(value): + raise FileConstraintError("Non-existing file : %s"%value) + return + diff --git a/tutorius/core.py b/tutorius/core.py new file mode 100644 index 0000000..dd2435e --- /dev/null +++ b/tutorius/core.py @@ -0,0 +1,528 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 2 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 +""" +Core + +This module contains the core classes for tutorius + +""" + +import gtk +import logging +import copy +import os + +from sugar.tutorius.dialog import TutoriusDialog +from sugar.tutorius.gtkutils import find_widget +from sugar.tutorius.services import ObjectStore + +logger = logging.getLogger("tutorius") + +class Tutorial (object): + """ + Tutorial Class, used to run through the FSM. + """ + + def __init__(self, name, fsm,filename= None): + """ + Creates an unattached tutorial. + """ + object.__init__(self) + self.name = name + self.activity_init_state_filename = filename + + self.state_machine = fsm + self.state_machine.set_tutorial(self) + + self.state = None + + self.handlers = [] + self.activity = None + #Rest of initialisation happens when attached + + def attach(self, activity): + """ + Attach to a running activity + + @param activity the activity to attach to + """ + #For now, absolutely detach if a previous one! + if self.activity: + self.detach() + self.activity = activity + ObjectStore().activity = activity + ObjectStore().tutorial = self + self._prepare_activity() + self.state_machine.set_state("INIT") + + def detach(self): + """ + Detach from the current activity + """ + + # Uninstall the whole FSM + self.state_machine.teardown() + + #FIXME There should be some amount of resetting done here... + self.activity = None + + + def set_state(self, name): + """ + Switch to a new state + """ + logger.debug("==== NEW STATE: %s ====" % name) + + self.state_machine.set_state(name) + + + # Currently unused -- equivalent function is in each state + def _eventfilter_state_done(self, eventfilter): + """ + Callback handler for eventfilter to notify + when we must go to the next state. + """ + #XXX Tests should be run here normally + + #Swith to the next state pointed by the eventfilter + self.set_state(eventfilter.get_next_state()) + + def _prepare_activity(self): + """ + Prepare the activity for the tutorial by loading the saved state and + emitting gtk signals + """ + #Load the saved activity if any + if self.activity_init_state_filename is not None: + #For now the file will be saved in the data folder + #of the activity root directory + filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\ + self.activity_init_state_filename + if os.path.exists(filename): + self.activity.read_file(filename) + + +class State(object): + """ + This is a step in a tutorial. The state represents a collection of actions + to undertake when entering the state, and a series of event filters + with associated actions that point to a possible next state. + """ + + def __init__(self, name="", action_list=None, event_filter_list=None, tutorial=None): + """ + Initializes the content of the state, like loading the actions + that are required and building the correct tests. + + @param action_list The list of actions to execute when entering this + state + @param event_filter_list A list of tuples of the form + (event_filter, next_state_name), that explains the outgoing links for + this state + @param tutorial The higher level container of the state + """ + object.__init__(self) + + self.name = name + + self._actions = action_list or [] + + # Unused for now + #self.tests = [] + + self._event_filters = event_filter_list or [] + + self.tutorial = tutorial + + def set_tutorial(self, tutorial): + """ + Associates this state with a tutorial. A tutorial must be set prior + to executing anything in the state. The reason for this is that the + states need to have access to the activity (via the tutorial) in order + to properly register their callbacks on the activities' widgets. + + @param tutorial The tutorial that this state runs under. + """ + if self.tutorial == None : + self.tutorial = tutorial + else: + raise RuntimeWarning(\ + "The state %s was already associated with a tutorial." % self.name) + + def setup(self): + """ + Install the state itself, by first registering the event filters + and then triggering the actions. + """ + for eventfilter in self._event_filters: + eventfilter.install_handlers(self._event_filter_state_done_cb, + activity=self.tutorial.activity) + + for action in self._actions: + action.do() + + def teardown(self): + """ + Uninstall all the event filters that were active in this state. + Also undo every action that was installed for this state. This means + removing dialogs that were displayed, removing highlights, etc... + """ + # Remove the handlers for the all of the state's event filters + for event_filter in self._event_filters: + event_filter.remove_handlers() + + # Undo all the actions related to this state + for action in self._actions: + action.undo() + + def _event_filter_state_done_cb(self, event_filter): + """ + Callback for event filters. This function needs to inform the + tutorial that the state is over and tell it what is the next state. + + @param event_filter The event filter that was called + """ + # Run the tests here, if need be + + # Warn the higher level that we wish to change state + self.tutorial.set_state(event_filter.get_next_state()) + + # Model manipulation + # These functions are used to simplify the creation of states + def add_action(self, new_action): + """ + Adds an action to the state (only if it wasn't added before) + + @param new_action The new action to execute when in this state + @return True if added, False otherwise + """ + if new_action not in self._actions: + self._actions.append(new_action) + return True + return False + + # remove_action - We did not define names for the action, hence they're + # pretty hard to remove on a precise basis + + def get_action_list(self): + """ + @return A list of actions that the state will execute + """ + return self._actions + + def clear_actions(self): + """ + Removes all the action associated with this state. A cleared state will + not do anything when entered or exited. + """ + self._actions = [] + + def add_event_filter(self, event_filter): + """ + Adds an event filter that will cause a transition from this state. + + The same event filter may not be added twice. + + @param event_filter The new event filter that will trigger a transition + @return True if added, False otherwise + """ + if event_filter not in self._event_filters: + self._event_filters.append(event_filter) + return True + return False + + def get_event_filter_list(self): + """ + @return The list of event filters associated with this state. + """ + return self._event_filters + + def clear_event_filters(self): + """ + Removes all the event filters associated with this state. A state that + was just cleared will become a sink and will be the end of the + tutorial. + """ + self._event_filters = [] + +class FiniteStateMachine(State): + """ + This is a collection of states, with a start state and an end callback. + It is used to simplify the development of the various tutorials by + encapsulating a collection of states that represent a given learning + process. + + For now, we will consider that there can only be states + inserted in the FSM, and that there are no nested FSM inside. + """ + + def __init__(self, name, tutorial=None, state_dict=None, start_state_name="INIT", action_list=None): + """ + The constructor for a FSM. Pass in the start state and the setup + actions that need to be taken when the FSM itself start (which may be + different from what is done in the first state of the machine). + + @param name A short descriptive name for this FSM + @param tutorial The tutorial that will execute this FSM. If None is + attached on creation, then one must absolutely be attached before + executing the FSM with set_tutorial(). + @param state_dict A dictionary containing the state names as keys and + the state themselves as entries. + @param start_state_name The name of the starting state, if different + from "INIT" + @param action_list The actions to undertake when initializing the FSM + """ + State.__init__(self, name) + + self.name = name + self.tutorial = tutorial + + # Dictionnary of states contained in the FSM + self._states = state_dict or {} + + self.start_state_name = start_state_name + # Set the current state to None - we are not executing anything yet + self.current_state = None + + # Register the actions for the FSM - They will be processed at the + # FSM level, meaning that when the FSM will start, it will first + # execute those actions. When the FSM closes, it will tear down the + # inner actions of the state, then close its own actions + self.actions = action_list or [] + + # Flag to mention that the FSM was initialized + self._fsm_setup_done = False + # Flag that must be raised when the FSM is to be teared down + self._fsm_teardown_done = False + # Flag used to declare that the FSM has reached an end state + self._fsm_has_finished = False + + def set_tutorial(self, tutorial): + """ + This associates the FSM to the given tutorial. It MUST be associated + either in the constructor or with this function prior to executing the + FSM. + + @param tutorial The tutorial that will execute this FSM. + """ + # If there was no tutorial associated + if self.tutorial == None: + # Associate it with this FSM and all the underlying states + self.tutorial = tutorial + for state in self._states.itervalues(): + state.set_tutorial(tutorial) + else: + raise RuntimeWarning(\ + "The FSM %s is already associated with a tutorial."%self.name) + + def setup(self): + """ + This function initializes the FSM the first time it is called. + Then, every time it is called, it initializes the current state. + """ + # Are we associated with a tutorial? + if self.tutorial == None: + raise UnboundLocalError("No tutorial was associated with FSM %s" % self.name) + + # If we never initialized the FSM itself, then we need to run all the + # actions associated with the FSM. + if self._fsm_setup_done == False: + # Remember the initial state - we might want to reset + # or rewind the FSM at a later moment + self.start_state = self._states[self.start_state_name] + self.current_state = self.start_state + # Flag the FSM level setup as done + self._fsm_setup_done = True + # Execute all the FSM level actions + for action in self.actions: + action.do() + + # Then, we need to run the setup of the current state + self.current_state.setup() + + def set_state(self, new_state_name): + """ + This functions changes the current state of the finite state machine. + + @param new_state The identifier of the state we need to go to + """ + # TODO : Since we assume no nested FSMs, we don't set state on the + # inner States / FSMs +## # Pass in the name to the internal state - it might be a FSM and +## # this name will apply to it +## self.current_state.set_state(new_state_name) + + # Make sure the given state is owned by the FSM + if not self._states.has_key(new_state_name): + # If we did not recognize the name, then we do not possess any + # state by that name - we must ignore this state change request as + # it will be done elsewhere in the hierarchy (or it's just bogus). + return + + if self.current_state != None: + if new_state_name == self.current_state.name: + # If we already are in this state, we do not need to change + # anything in the current state - By design, a state may not point + # to itself + return + + new_state = self._states[new_state_name] + + # Undo the actions of the old state + self.teardown() + + # Insert the new state + self.current_state = new_state + + # Call the initial actions in the new state + self.setup() + + def get_current_state_name(self): + """ + Returns the name of the current state. + + @return A string representing the name of the current state + """ + return self.current_state.name + + def teardown(self): + """ + Revert any changes done by setup() + """ + # Teardown the current state + if self.current_state is not None: + self.current_state.teardown() + + # If we just finished the whole FSM, we need to also call the teardown + # on the FSM level actions + if self._fsm_has_finished == True: + # Flag the FSM teardown as not needed anymore + self._fsm_teardown_done = True + # Undo all the FSM level actions here + for action in self.actions: + action.undo() + + # TODO : It might be nice to have a start() and stop() method for the + # FSM. + + # Data manipulation section + # These functions are dedicated to the building and editing of a graph. + def add_state(self, new_state): + """ + Inserts a new state in the FSM. + + @param new_state The State object that will now be part of the FSM + @raise KeyError In the case where a state with this name already exists + """ + if self._states.has_key(new_state.name): + raise KeyError("There is already a state by this name in the FSM") + + self._states[new_state.name] = new_state + + # Not such a great name for the state accessor... We already have a + # set_state name, so get_state would conflict with the notion of current + # state - I would recommend having a set_current_state instead. + def get_state_by_name(self, state_name): + """ + Fetches a state from the FSM, based on its name. If there is no + such state, the method will throw a KeyError. + + @param state_name The name of the desired state + @return The State object having the given name + """ + return self._states[state_name] + + def remove_state(self, state_name): + """ + Removes a state from the FSM. Raises a KeyError when the state is + not existent. + + Warning : removing a state will also remove all the event filters that + point to this given name, to preserve the FSM's integrity. If you only + want to edit a state, you would be better off fetching this state with + get_state_by_name(). + + @param state_name A string being the name of the state to remove + @raise KeyError When the state_name does not a represent a real state + stored in the dictionary + """ + + state_to_remove = self._states[state_name] + + # Remove the state from the states' dictionnary + for st in self._states.itervalues(): + # Iterate through the list of event filters and remove those + # that point to the state that will be removed + + #TODO : Move this code inside the State itself - we're breaking + # encap :P + for event_filter in st._event_filters: + if event_filter.get_next_state() == state_name: + st._event_filters.remove(event_filter) + + # Remove the state from the dictionary + del self._states[state_name] + + # Exploration methods - used to know more about a given state + def get_following_states(self, state_name): + """ + Returns a tuple of the names of the states that point to the given + state. If there is no such state, the function raises a KeyError. + + @param state_name The name of the state to analyse + @raise KeyError When there is no state by this name in the FSM + """ + state = self._states[state_name] + + next_states = set() + + for event_filter in state._event_filters: + next_states.add(event_filter.get_next_state()) + + return tuple(next_states) + + def get_previous_states(self, state_name): + """ + Returns a tuple of the names of the state that can transition to + the given state. If there is no such state, the function raises a + KeyError. + + @param state_name The name of the state that the returned states might + transition to. + """ + # This might seem a bit funny, but we don't verify if the given + # state is present or not in the dictionary. + # This is due to the fact that when building a graph, we might have a + # prototypal state that has not been inserted yet. We could not know + # which states are pointing to it until we insert it in the graph. + + states = [] + # Walk through the list of states + for st in self._states.itervalues(): + for event_filter in st._event_filters: + if event_filter.get_next_state() == state_name: + states.append(event_filter.get_next_state()) + continue + + return tuple(states) + + # Convenience methods to see the content of a FSM + def __str__(self): + out_string = "" + for st in self._states.itervalues(): + out_string += st.name + ", " + return out_string 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: diff --git a/tutorius/dialog.py b/tutorius/dialog.py new file mode 100644 index 0000000..be51a0e --- /dev/null +++ b/tutorius/dialog.py @@ -0,0 +1,59 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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 +""" +The Dialog module provides means of interacting with the user +through the use of Dialogs. +""" +import gtk + +class TutoriusDialog(gtk.Dialog): + """ + TutoriusDialog is a simple wrapper around gtk.Dialog. + + It allows creating and showing a dialog and connecting the response and + button click events to callbacks. + """ + def __init__(self, label="Hint", button_clicked_cb=None, response_cb=None): + """ + Constructor. + + @param label text to be shown on the dialog + @param button_clicked_cb callback for the button click + @param response_cb callback for the dialog response + """ + gtk.Dialog.__init__(self) + + self._button = gtk.Button(label) + + self.add_action_widget(self._button, 1) + + if not button_clicked_cb == None: + self._button.connect("clicked", button_clicked_cb) + + self._button.show() + + if not response_cb == None: + self.connect("response", response_cb) + + self.set_decorated(False) + + def set_button_clicked_cb(self, funct): + """Setter for the button_clicked callback""" + self._button.connect("clicked", funct) + + def close_self(self, arg=None): + """Close the dialog""" + self.destroy() diff --git a/tutorius/editor.py b/tutorius/editor.py new file mode 100644 index 0000000..42cc718 --- /dev/null +++ b/tutorius/editor.py @@ -0,0 +1,318 @@ +# Copyright (C) 2009, Tutorius.org +# Greatly influenced by sugar/activity/namingalert.py +# +# 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 2 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 +""" Tutorial Editor Module +""" + +import gtk +import gobject +#import hippo +#import gconf + +from gettext import gettext as _ + +from sugar.tutorius.gtkutils import register_signals_numbered, get_children + +class WidgetIdentifier(gtk.Window): + """ + Tool that allows identifying widgets. + + """ + __gtype_name__ = 'TutoriusWidgetIdentifier' + + def __init__(self, activity): + gtk.Window.__init__(self) + + self._activity = activity + self._handlers = {} + # dict of signals to register on the widgets. + # key : signal name + # value : initial checkbox status + signals = { + "focus":True, + "button-press-event":True, + "enter-notify-event":False, + "leave-notify-event":False, + "key-press-event":True, + "text-selected":True, + "clicked":True, + } + + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(False) + + self.connect('realize', self.__realize_cb) + + self._expander = gtk.Expander(_("Widget Identifier")) + self._expander.set_expanded(True) + self.add(self._expander) + self._expander.connect("notify::expanded", self.__expander_cb) + + self._expander.show() + + nbk = gtk.Notebook() + self._expander.add(nbk) + nbk.show() + + ############################### + # Event log viewer page + ############################### + self.logview = gtk.TextView() + self.logview.set_editable(False) + self.logview.set_cursor_visible(False) + self.logview.set_wrap_mode(gtk.WRAP_NONE) + self._textbuffer = self.logview.get_buffer() + + swd = gtk.ScrolledWindow() + swd.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + swd.add(self.logview) + self.logview.show() + + nbk.append_page(swd, gtk.Label(_("Log"))) + swd.show() + + ############################### + # Filters page + ############################### + filters = gtk.Table( (len(signals)+1)/2, 2) + + xpos, ypos = 0, 0 + for key, active in signals.items(): + cbtn = gtk.CheckButton(label=key) + filters.attach(cbtn, xpos, xpos+1, ypos, ypos+1) + cbtn.show() + cbtn.set_active(active) + if active: + self._handlers[key] = register_signals_numbered( \ + self._activity, self._handle_events, events=(key,)) + else: + self._handlers[key] = [] + + cbtn.connect("toggled", self.__filter_toggle_cb, key) + + #Follow lines then columns + xpos, ypos = (xpos+1)%2, ypos+(xpos%2) + + nbk.append_page(filters, gtk.Label(_("Events"))) + filters.show() + + ############################### + # Explorer Page + ############################### + tree = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + explorer = gtk.TreeView(tree) + + pathrendr = gtk.CellRendererText() + pathrendr.set_properties(background="#ffffff", foreground="#000000") + pathcol = gtk.TreeViewColumn(_("Path"), pathrendr, text=0, background=0, foreground=0) + explorer.append_column(pathcol) + + typerendr = gtk.CellRendererText() + typerendr.set_properties(background="#ffffff", foreground="#000000") + typecol = gtk.TreeViewColumn(_("Widget"), typerendr, text=1, background=1, foreground=1) + explorer.append_column(typecol) + + self.__populate_treestore( + tree, #tree + tree.append(None, ["0",self._activity.get_name()]), #parent + self._activity, #widget + "0" #path + ) + + explorer.set_expander_column(typecol) + + swd2 = gtk.ScrolledWindow() + swd2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + swd2.add(explorer) + explorer.show() + nbk.append_page(swd2, gtk.Label(_("Explorer"))) + swd2.show() + + ############################### + # GObject Explorer Page + ############################### + tree2 = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + explorer2 = gtk.TreeView(tree2) + + pathrendr2 = gtk.CellRendererText() + pathrendr2.set_properties(background="#ffffff", foreground="#000000") + pathcol2 = gtk.TreeViewColumn(_("Path"), pathrendr2, text=0, background=0, foreground=0) + explorer2.append_column(pathcol2) + + typerendr2 = gtk.CellRendererText() + typerendr2.set_properties(background="#ffffff", foreground="#000000") + typecol2 = gtk.TreeViewColumn(_("Widget"), typerendr2, text=1, background=1, foreground=1) + explorer2.append_column(typecol2) + + self.__populate_gobject_treestore( + tree2, #tree + tree2.append(None, ["activity",self._activity.get_name()]), #parent + self._activity, #widget + "activity" #path + ) + + explorer2.set_expander_column(typecol2) + + swd3 = gtk.ScrolledWindow() + swd3.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + swd3.add(explorer2) + explorer2.show() + nbk.append_page(swd3, gtk.Label(_("GObject Explorer"))) + swd3.show() + + def __populate_treestore(self, tree, parent, widget, path): + """Populates the treestore with the widget's children recursively + @param tree gtk.TreeStore to populate + @param parent gtk.TreeIter to append to + @param widget gtk.Widget to check for children + @param path treeish of the widget + """ + #DEBUG: show parameters in log window gehehe + #self._handle_events((path,str(type(widget)))) + children = get_children(widget) + for i in xrange(len(children)): + childpath = ".".join([path, str(i)]) + child = children[i] + self.__populate_treestore( + tree, #tree + tree.append(parent, [childpath, child.get_name()]), #parent + child, #widget + childpath #path + ) + + + def __populate_gobject_treestore(self, tree, parent, widget, path, listed=None): + """Populates the treestore with the widget's children recursively + @param tree gtk.TreeStore to populate + @param parent gtk.TreeIter to append to + @param widget gtk.Widget to check for children + @param path treeish of the widget + """ + listed = listed or [] + if widget in listed: + return + listed.append(widget) + #DEBUG: show parameters in log window gehehe + #self._handle_events((path,str(type(widget)))) + #Add a child node + children = tree.append(parent, ["","children"]) + for i in dir(widget): + #Add if a gobject + try: + child = getattr(widget, i) + except: + continue + if isinstance(child,gobject.GObject): + childpath = ".".join([path, i]) + child = getattr(widget, i) + self.__populate_gobject_treestore( + tree, #tree + tree.append(children, [childpath, i]), #parent + child, #widget + path + "." + i, #path, + listed + ) + widgets = tree.append(parent, ["","widgets"]) + wchildren = get_children(widget) + for i in xrange(len(wchildren)): + childpath = ".".join([path, str(i)]) + child = wchildren[i] + self.__populate_gobject_treestore( + tree, #tree + tree.append(widgets, [childpath, (hasattr(child,"get_name") and child.get_name()) or i]), #parent + child, #widget + childpath, #path, + listed + ) + + #Add signals and attributes nodes + signals = tree.append(parent, ["","signals"]) + for signame in gobject.signal_list_names(widget): + tree.append(signals, ["",signame]) + + attributes = tree.append(parent, ["","properties"]) + for prop in gobject.list_properties(widget): + tree.append(attributes, ["",prop]) + + def __filter_toggle_cb(self, btn, eventname): + """Callback for signal name checkbuttons' toggling""" + #Disconnect existing handlers on key + self.__disconnect_handlers(eventname) + if btn.get_active(): + #if checked, reconnect + self._handlers[eventname] = register_signals_numbered( \ + self._activity, self._handle_events, events=(eventname,)) + + + def __expander_cb(self, *args): + """Callback for the window expander toggling""" + if self._expander.get_expanded(): + self.__move_expanded() + else: + self.__move_collapsed() + + def __move_expanded(self): + """Move the window to it's expanded position""" + width = 500 + height = 300 + swidth = gtk.gdk.screen_width() + sheight = gtk.gdk.screen_height() + + self.set_size_request(width, height) + self.move((swidth-width)/2, sheight-height) + + def __move_collapsed(self): + """Move the window to it's collapsed position""" + width = 150 + height = 40 + swidth = gtk.gdk.screen_width() + sheight = gtk.gdk.screen_height() + + self.set_size_request(width, height) + self.move((swidth-width)/2, sheight-height) + + def __realize_cb(self, widget): + """Callback for realize""" + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + self.__move_expanded() + + def _disconnect_handlers(self): + """ Disconnect all event handlers """ + for key in self._handlers: + self.__disconnect_handlers(key) + + def __disconnect_handlers(self, key): + """ Disconnect event handlers associated to signal name "key" """ + if self._handlers.has_key(key): + for widget, handlerid in self._handlers[key]: + widget.handler_disconnect(handlerid) + del self._handlers[key] + + def _handle_events(self, *args): + """ Event handler for subscribed widget events. + Accepts variable length argument list. Last must be + a two-tuple containing (event name, widget name) """ + sig, name = args[-1] + text = "\r\n".join( + (["%s event received from %s" % (sig, name)] + + self._textbuffer.get_text(*(self._textbuffer.get_bounds()) + ).split("\r\n"))[:80] + ) + self._textbuffer.set_text(text) + + diff --git a/tutorius/filters.py b/tutorius/filters.py new file mode 100644 index 0000000..aa8c997 --- /dev/null +++ b/tutorius/filters.py @@ -0,0 +1,204 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 2 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 gobject +import gtk +import logging +logger = logging.getLogger("filters") + +from sugar.tutorius.gtkutils import find_widget +from sugar.tutorius.services import ObjectStore +from sugar.tutorius import properties + + +class EventFilter(properties.TPropContainer): + """ + Base class for an event filter + """ + + next_state = properties.TStringProperty("None") + + def __init__(self, next_state=None): + """ + Constructor. + @param next_state name of the next state + """ + super(EventFilter, self).__init__() + if next_state: + self.next_state = next_state + self._callback = None + + def get_next_state(self): + """ + Getter for the next state + """ + return self.next_state + + def set_next_state(self, new_next_name): + """ + Setter for the next state. Should only be used during construction of + the event_fitler, not while the tutorial is running. + """ + self.next_state = new_next_name + + def install_handlers(self, callback, **kwargs): + """ + install_handlers is called for eventfilters to setup all + necessary event handlers to be able to catch the desired + event. + + @param callback the callback function that will be called + with the event filter as an argument when the event is catched + and validated. + @param **kwargs unused by this handler for now, allows subclasses + to receive information about the context when installing + + Subclasses must call this super method to setup the callback if they + feel like cooperating + """ + self._callback = callback + + def remove_handlers(self): + """ + remove_handlers is called when a state is done so that all + event filters can cleanup any handlers they have installed + + This function will also clear the callback function so that any + leftover handler that is triggered will not be able to change the + application state. + + subclasses must call this super method to cleanup the callback if they + collaborate and use this classe's do_callback() + """ + self._callback = None + + def do_callback(self, *args, **kwargs): + """ + Default callback function that calls the event filter callback + with the event filter as only argument. + """ + if self._callback: + self._callback(self) + +class TimerEvent(EventFilter): + """ + TimerEvent is a special EventFilter that uses gobject + timeouts to trigger a state change after a specified amount + of time. It must be used inside a gobject main loop to work. + """ + def __init__(self,next_state,timeout_s): + """Constructor. + + @param next_state default EventFilter param, passed on to EventFilter + @param timeout_s timeout in seconds + """ + super(TimerEvent,self).__init__(next_state) + self._timeout = timeout_s + self._handler_id = None + + def install_handlers(self, callback, **kwargs): + """install_handlers creates the timer and starts it""" + super(TimerEvent,self).install_handlers(callback, **kwargs) + #Create the timer + self._handler_id = gobject.timeout_add_seconds(self._timeout, self._timeout_cb) + + def remove_handlers(self): + """remove handler removes the timer""" + super(TimerEvent,self).remove_handlers() + if self._handler_id: + try: + #XXX What happens if this was already triggered? + #remove the timer + gobject.source_remove(self._handler_id) + except: + pass + + def _timeout_cb(self): + """ + _timeout_cb triggers the eventfilter callback. + + It is necessary because gobject timers only stop if the callback they + trigger returns False + """ + self.do_callback() + return False #Stops timeout + +class GtkWidgetTypeFilter(EventFilter): + """ + Event Filter that listens for keystrokes on a widget + """ + def __init__(self, next_state, object_id, text=None, strokes=None): + """Constructor + @param next_state default EventFilter param, passed on to EventFilter + @param object_id object tree-ish identifier + @param text resulting text expected + @param strokes list of strokes expected + + At least one of text or strokes must be supplied + """ + super(GtkWidgetTypeFilter, self).__init__(next_state) + self._object_id = object_id + self._text = text + self._captext = "" + self._strokes = strokes + self._capstrokes = [] + self._widget = None + self._handler_id = None + + def install_handlers(self, callback, **kwargs): + """install handlers + @param callback default EventFilter callback arg + """ + super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs) + logger.debug("~~~GtkWidgetTypeFilter install") + activity = ObjectStore().activity + if activity is None: + logger.error("No activity") + raise RuntimeWarning("no activity in the objectstore") + + self._widget = find_widget(activity, self._object_id) + if self._widget: + self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb) + logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) ) + + def remove_handlers(self): + """remove handlers""" + super(GtkWidgetTypeFilter, self).remove_handlers() + #if an event was connected, disconnect it + if self._handler_id: + self._widget.handler_disconnect(self._handler_id) + self._handler_id=None + + def __keypress_cb(self, widget, event, *args): + """keypress callback""" + logger.debug("~~~keypressed!") + key = event.keyval + keystr = event.string + logger.debug("~~~Got key: " + str(key) + ":"+ keystr) + self._capstrokes += [key] + #TODO Treat other stuff, such as arrows + if key == gtk.keysyms.BackSpace: + self._captext = self._captext[:-1] + else: + self._captext = self._captext + keystr + + logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext)) + if not self._strokes is None and self._strokes in self._capstrokes: + self.do_callback() + if not self._text is None and self._text in self._captext: + self.do_callback() + diff --git a/tutorius/gtkutils.py b/tutorius/gtkutils.py new file mode 100644 index 0000000..1a9cb0f --- /dev/null +++ b/tutorius/gtkutils.py @@ -0,0 +1,203 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 2 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 +""" +Utility classes and functions that are gtk related +""" +import gtk + +def raddr_lookup(widget): + name = [] + child = widget + parent = widget.parent + while parent: + name.append(str(parent.get_children().index(child))) + child = parent + parent = child.parent + + name.append("0") # root object itself + name.reverse() + return ".".join(name) + + +def find_widget(base, target_fqdn): + """Find a widget by digging into a parent widget's children tree + @param base the parent widget + @param target_fqdn fqdn-style target object name + + @return widget found + + The object should normally be the activity widget, as it is the root + widget for activities. The target_fqdn is a dot separated list of + indexes used in widget.get_children and should start with a 0 which is + the base widget itself, + + Example Usage: + find_widget(activity,"0.0.0.1.0.0.2") + """ + path = target_fqdn.split(".") + #We select the first object and pop the first zero + obj = base + path.pop(0) + + while len(path) > 0: + try: + obj = get_children(obj)[int(path.pop(0))] + except: + break + + return obj + +EVENTS = ( + "focus", + "button-press-event", + "enter-notify-event", + "leave-notify-event", + "key-press-event", + "text-selected", + "clicked", +) + +IGNORED_WIDGETS = ( + "GtkVBox", + "GtkHBox", + "GtkAlignment", + "GtkNotebook", + "GtkButton", + "GtkToolItem", + "GtkToolbar", +) + +def register_signals_numbered(target, handler, prefix="0", max_depth=None, events=None): + """ + Recursive function to register event handlers on an target + and it's children. The event handler is called with an extra + argument which is a two-tuple containing the signal name and + the FQDN-style name of the target that triggered the event. + + This function registers all of the events listed in + EVENTS + + Example arg tuple added: + ("focus", "1.1.2") + Side effects: + -Handlers connected on the various targets + + @param target the target to recurse on + @param handler the handler function to connect + @param prefix name prepended to the target name to form a chain + @param max_depth maximum recursion depth, None for infinity + + @returns list of (object, handler_id) + """ + ret = [] + evts = events or EVENTS + #Gtk Containers have a get_children() function + children = get_children(target) + for i in range(len(children)): + child = children[i] + if max_depth is None or max_depth > 0: + #Recurse with a prefix on all children + pre = ".".join( \ + [p for p in (prefix, str(i)) if not p is None] + ) + if max_depth is None: + dep = None + else: + dep = max_depth - 1 + ret+=register_signals_numbered(child, handler, pre, dep, evts) + #register events on the target if a widget XXX necessary to check this? + if isinstance(target, gtk.Widget): + for sig in evts: + try: + ret.append( \ + (target, target.connect(sig, handler, (sig, prefix) ))\ + ) + except TypeError: + pass + + return ret + +def register_signals(target, handler, prefix=None, max_depth=None, events=None): + """ + Recursive function to register event handlers on an target + and it's children. The event handler is called with an extra + argument which is a two-tuple containing the signal name and + the FQDN-style name of the target that triggered the event. + + This function registers all of the events listed in + EVENTS and omits widgets with a name matching + IGNORED_WIDGETS from the name hierarchy. + + Example arg tuple added: + ("focus", "Activity.Toolbox.Bold") + Side effects: + -Handlers connected on the various targets + + @param target the target to recurse on + @param handler the handler function to connect + @param prefix name prepended to the target name to form a chain + @param max_depth maximum recursion depth, None for infinity + + @returns list of (object, handler_id) + """ + ret = [] + evts = events or EVENTS + #Gtk Containers have a get_children() function + for child in get_children(target): + if max_depth is None or max_depth > 0: + #Recurse with a prefix on all children + pre = ".".join( \ + [p for p in (prefix, target.get_name()) \ + if not (p is None or p in IGNORED_WIDGETS)] \ + ) + if max_depth is None: + dep = None + else: + dep = max_depth - 1 + ret += register_signals(child, handler, pre, dep, evts) + name = ".".join( \ + [p for p in (prefix, target.get_name()) \ + if not (p is None or p in IGNORED_WIDGETS)] \ + ) + #register events on the target if a widget XXX necessary to check this? + if isinstance(target, gtk.Widget): + for sig in evts: + try: + ret.append( \ + (target, target.connect(sig, handler, (sig, name) )) \ + ) + except TypeError: + pass + + return ret + +def get_children(widget): + """Lists widget's children""" + #widgets with multiple children + try: + return widget.get_children() + except (AttributeError,TypeError): + pass + + #widgets with a single child + try: + return [widget.get_child(),] + except (AttributeError,TypeError): + pass + + #otherwise return empty list + return [] diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py new file mode 100644 index 0000000..91b11f4 --- /dev/null +++ b/tutorius/linear_creator.py @@ -0,0 +1,95 @@ +# Copyright (C) 2009, Tutorius.org +# Greatly influenced by sugar/activity/namingalert.py +# +# 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 2 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 + +from sugar.tutorius.core import * +from sugar.tutorius.actions import * +from sugar.tutorius.filters import * + +from copy import deepcopy + +class LinearCreator(object): + """ + This class is used to create a FSM from a linear sequence of orders. The + orders themselves are meant to be either an action or a transition. + """ + + def __init__(self): + self.fsm = FiniteStateMachine("Sample Tutorial") + self.current_actions = [] + self.nb_state = 0 + self.state_name = "INIT" + + def set_name(self, name): + """ + Sets the name of the generated FSM. + """ + self.fsm.name = name + + def action(self, action): + """ + Adds an action to execute in the current state. + """ + self.current_actions.append(action) + + def event(self, event_filter): + """ + Adds a transition to another state. When executing this, all the actions + previously called will be bundled in a single state, with the exit + condition of this state being the transition just added. + + Whatever the name of the next state you inserted in the event, it will + be replaced to point to the next event in the line. + """ + if len(self.current_actions) != 0: + # Set the next state name - there is no way the caller should have + # to deal with that. + next_state_name = "State %d" % (self.nb_state+1) + event_filter.set_next_state(next_state_name) + state = State(self.state_name, action_list=self.current_actions, + event_filter_list=[event_filter]) + self.state_name = next_state_name + + self.nb_state += 1 + self.fsm.add_state(state) + + # Clear the actions from the list + self.current_actions = [] + + def generate_fsm(self): + """ + Returns a finite state machine corresponding to the sequence of calls + that were made from this point on. + """ + # Copy the whole FSM that was generated yet + new_fsm = deepcopy(self.fsm) + + # Generate the final state + state = None + if len(self.current_actions) != 0: + state = State("State" + str(self.nb_state), action_list=self.current_actions) + # Don't increment the nb_state here - we would break the linearity + # because we might generate more stuff with this creator later. + # Since we rely on linearity for continuity when generating the + # next state's name on an event filter, we cannot increment here. + else: + state = State("State" + str(self.nb_state)) + + # Insert the state in the copy of the FSM + new_fsm.add_state(state) + + return new_fsm + diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py new file mode 100644 index 0000000..931949d --- /dev/null +++ b/tutorius/overlayer.py @@ -0,0 +1,503 @@ +""" +This module manages drawing of overlayed widgets. The class responsible for +drawing management (Overlayer) and basic overlayable widgets are defined here. +""" +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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 gobject +import gtk +import cairo +import pangocairo +from math import pi + +from sugar import profile + +# for easy profile access from cairo +color = profile.get_color().get_stroke_color() +xo_line_color = (int(color[1:3], 16)/255.0, + int(color[3:5], 16)/255.0, + int(color[5:7], 16)/255.0) +color = profile.get_color().get_fill_color() +xo_fill_color = (int(color[1:3], 16)/255.0, + int(color[3:5], 16)/255.0, + int(color[5:7], 16)/255.0) +del color + +# This is the CanvasDrawable protocol. Any widget wishing to be drawn on the +# overlay must implement it. See TextBubble for a sample implementation. +#class CanvasDrawable(object): +# """Defines the CanvasDrawable protocol""" +# no_expose = None +# def draw_with_context(self, context): +# """ +# Draws the cairo widget with the passed cairo context. +# This will be called if the widget is child of an overlayer. +# """ +# pass + +class Overlayer(gtk.Layout): + """ + This guy manages drawing of overlayed widgets. Those can be standard GTK + widgets or special "cairoDrawable" widgets which support the defined + interface (see the put method). + + @param overlayed widget to be overlayed. Will be resized to full size. + """ + def __init__(self, overlayed=None): + gtk.Layout.__init__(self) + + self._overlayed = overlayed + if overlayed: + self.put(overlayed, 0, 0) + + self.__realizer = self.connect("expose-event", self.__init_realized) + self.connect("size-allocate", self.__size_allocate) + self.show() + + self.__render_handle = None + + def put(self, child, x, y): + """ + Adds a child widget to be overlayed. This can be, overlay widgets or + normal GTK widgets (though normal widgets will alwas appear under + cairo widgets due to the rendering chain). + + @param child the child to add + @param x the horizontal coordinate for positionning + @param y the vertical coordinate for positionning + """ + if hasattr(child, "draw_with_context"): + # if the widget has the CanvasDrawable protocol, use it. + child.no_expose = True + gtk.Layout.put(self, child, x, y) + + # be sure to redraw or the overlay may not show + self.queue_draw() + + + def __init_realized(self, widget, event): + """ + Initializer to set once widget is realized. + Since an expose event is signaled only to realized widgets, we set this + callback for the first expose run. It should also be called after + beign reparented to ensure the window used for drawing is set up. + """ + assert hasattr(self.window, "set_composited"), \ + "compositing not supported or widget not realized." + self.disconnect(self.__realizer) + del self.__realizer + + self.parent.set_app_paintable(True) + + # the parent is composited, so we can access gtk's rendered buffer + # and overlay over. If we don't composite, we won't be able to read + # pixels and background will be black. + self.window.set_composited(True) + self.__render_handle = self.parent.connect_after("expose-event", \ + self.__expose_overlay) + + def __expose_overlay(self, widget, event): + """expose event handler to draw the thing.""" + #get our child (in this case, the event box) + child = widget.get_child() + + #create a cairo context to draw to the window + ctx = widget.window.cairo_create() + + #the source data is the (composited) event box + ctx.set_source_pixmap(child.window, + child.allocation.x, + child.allocation.y) + + #draw no more than our expose event intersects our child + region = gtk.gdk.region_rectangle(child.allocation) + rect = gtk.gdk.region_rectangle(event.area) + region.intersect(rect) + ctx.region (region) + ctx.clip() + + ctx.set_operator(cairo.OPERATOR_OVER) + # has to be blended and a 1.0 alpha would not make it blend + ctx.paint_with_alpha(0.99) + + #draw overlay + for drawn_child in self.get_children()[1:]: + if hasattr(drawn_child, "draw_with_context"): + drawn_child.draw_with_context(ctx) + + def __size_allocate(self, widget, allocation): + """ + Set size allocation (actual gtk widget size) and propagate it to + overlayed child + """ + self.allocation = allocation + # One may wonder why using size_request instead of size_allocate; + # Since widget is laid out in a Layout box, the Layout will honor the + # requested size. Using size_allocate could make a nasty nested loop in + # some cases. + self._overlayed.set_size_request(allocation.width, allocation.height) + + +class TextBubble(gtk.Widget): + """ + A CanvasDrawableWidget drawing a round textbox and a tail pointing + to a specified widget. + """ + def __init__(self, text, speaker=None, tailpos=[0,0]): + """ + Creates a new cairo rendered text bubble. + + @param text the text to render in the bubble + @param speaker the widget to compute the tail position from + @param tailpos (optional) position relative to the bubble to use as + the tail position, if no speaker + """ + gtk.Widget.__init__(self) + + # FIXME: ensure previous call does not interfere with widget stacking, + # as using a gtk.Layout and stacking widgets may reveal a screwed up + # order with the cairo widget on top. + self.__label = None + + self.label = text + self.speaker = speaker + self.tailpos = tailpos + self.line_width = 5 + self.padding = 20 + + self._no_expose = False + self.__exposer = None + + def draw_with_context(self, context): + """ + Draw using the passed cairo context instead of creating a new cairo + context. This eases blending between multiple cairo-rendered + widgets. + """ + context.translate(self.allocation.x, self.allocation.y) + width = self.allocation.width + height = self.allocation.height + xradius = width/2 + yradius = height/2 + width -= self.line_width + height -= self.line_width + # + # TODO fetch speaker coordinates + + # draw bubble tail if present + if self.tailpos != [0,0]: + context.move_to(xradius-width/4, yradius) + context.line_to(self.tailpos[0], self.tailpos[1]) + context.line_to(xradius+width/4, yradius) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_line_color) + context.stroke_preserve() + + # bubble border + context.move_to(width-self.padding, 0.0) + context.line_to(self.padding, 0.0) + context.arc_negative(self.padding, self.padding, self.padding, + 3*pi/2, pi) + context.line_to(0.0, height-self.padding) + context.arc_negative(self.padding, height-self.padding, self.padding, + pi, pi/2) + context.line_to(width-self.padding, height) + context.arc_negative(width-self.padding, height-self.padding, + self.padding, pi/2, 0) + context.line_to(width, self.padding) + context.arc_negative(width-self.padding, self.padding, self.padding, + 0.0, -pi/2) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_line_color) + context.stroke_preserve() + context.set_source_rgb(*xo_fill_color) + context.fill() + + # bubble painting. Redrawing the inside after the tail will combine + if self.tailpos != [0,0]: + context.move_to(xradius-width/4, yradius) + context.line_to(self.tailpos[0], self.tailpos[1]) + context.line_to(xradius+width/4, yradius) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_fill_color) + context.fill() + + context.set_source_rgb(1.0, 1.0, 1.0) + pangoctx = pangocairo.CairoContext(context) + self._text_layout.set_markup(self.__label) + text_size = self._text_layout.get_pixel_size() + pangoctx.move_to( + int((self.allocation.width-text_size[0])/2), + int((self.allocation.height-text_size[1])/2)) + pangoctx.show_layout(self._text_layout) + + # work done. Be kind to next cairo widgets and reset matrix. + context.identity_matrix() + + def do_realize(self): + """ Setup gdk window creation. """ + self.set_flags(gtk.REALIZED | gtk.NO_WINDOW) + self.window = self.get_parent_window() + if not self._no_expose: + self.__exposer = self.connect_after("expose-event", \ + self.__on_expose) + + def __on_expose(self, widget, event): + """Redraw event callback.""" + ctx = self.window.cairo_create() + + self.draw_with_context(ctx) + + return True + + def _set_label(self, value): + """Sets the label and flags the widget to be redrawn.""" + self.__label = "%s"%value + if not self.parent: + return + ctx = self.parent.window.cairo_create() + pangoctx = pangocairo.CairoContext(ctx) + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(value) + del pangoctx, ctx#, surf + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the widget.""" + ctx = self.parent.window.cairo_create() + pangoctx = pangocairo.CairoContext(ctx) + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(self.__label) + + width, height = self._text_layout.get_pixel_size() + requisition.width = int(width+2*self.padding) + requisition.height = int(height+2*self.padding) + + def do_size_allocate(self, allocation): + """Save zone allocated to the widget.""" + self.allocation = allocation + + def _get_label(self): + """Getter method for the label property""" + return self.__label[3:-4] + + def _set_no_expose(self, value): + """setter for no_expose property""" + self._no_expose = value + if not (self.flags() and gtk.REALIZED): + return + + if self.__exposer and value: + self.parent.disconnect(self.__exposer) + self.__exposer = None + elif (not self.__exposer) and (not value): + self.__exposer = self.parent.connect_after("expose-event", + self.__on_expose) + + def _get_no_expose(self): + """getter for no_expose property""" + return self._no_expose + + no_expose = property(fset=_set_no_expose, fget=_get_no_expose, + doc="Whether the widget should handle exposition events or not.") + + label = property(fget=_get_label, fset=_set_label, + doc="Text label which is to be painted on the top of the widget") + +gobject.type_register(TextBubble) + +class Rectangle(gtk.Widget): + """ + A CanvasDrawableWidget drawing a rectangle over a specified widget. + """ + def __init__(self, widget, color): + """ + Creates a new Rectangle + + @param widget the widget to cover + @param color the color of the rectangle, as a 4-tuple RGBA + """ + gtk.Widget.__init__(self) + + self.covered = widget + self.color = color + + self.__exposer = self.connect("expose-event", self.__on_expose) + + def draw_with_context(self, context): + """ + Draw using the passed cairo context instead of creating a new cairo + context. This eases blending between multiple cairo-rendered + widgets. + """ + if self.covered is None: + # nothing to hide, no coordinates, no drawing + return + mask_alloc = self.covered.get_allocation() + x, y = self.covered.translate_coordinates(self.parent, 0, 0) + + context.rectangle(x, y, mask_alloc.width, mask_alloc.height) + context.set_source_rgba(*self.color) + context.fill() + + def do_realize(self): + """ Setup gdk window creation. """ + self.set_flags(gtk.REALIZED | gtk.NO_WINDOW) + + self.window = self.get_parent_window() + if not isinstance(self.parent, Overlayer): + assert False, "%s should not realize" % type(self).__name__ + print "Danger, Will Robinson! Rectangle parent is not Overlayer" + + def __on_expose(self, widget, event): + """Redraw event callback.""" + assert False, "%s wasn't meant to be exposed by gtk" % \ + type(self).__name__ + ctx = self.window.cairo_create() + + self.draw_with_context(ctx) + + return True + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the masked widget.""" + # This is a bit pointless, as this will always ignore allocation and + # be rendered directly on overlay, but for sanity, let's put some values + # in there. + if not self.covered: + requisition.width = 0 + requisition.height = 0 + return + + masked_alloc = self.covered.get_allocation() + requisition.width = masked_alloc.width + requisition.height = masked_alloc.height + + def do_size_allocate(self, allocation): + """Save zone allocated to the widget.""" + self.allocation = allocation + + def _set_no_expose(self, value): + """setter for no_expose property""" + if self.__exposer and value: + self.disconnect(self.__exposer) + self.__exposer = None + elif (not self.__exposer) and (not value): + self.__exposer = self.connect("expose-event", self.__on_expose) + + def _get_no_expose(self): + """getter for no_expose property""" + return not self.__exposer + + no_expose = property(fset=_set_no_expose, fget=_get_no_expose, + doc="Whether the widget should handle exposition events or not.") +gobject.type_register(Rectangle) + +class Mask(gtk.EventBox): + """ + A CanvasDrawableWidget drawing a rectangle over a specified widget. + """ + def __init__(self, catch_events=False, pass_thru=()): + """ + Creates a new Rectangle + + @param catch_events whether the Mask should catch events + @param pass_thru the widgets that "punch holes" through this Mask. + Events will pass through to those widgets. + """ + gtk.EventBox.__init__(self) + self.no_expose = True # ignored + self._catch_events = False + self.catch_events = catch_events + self.pass_thru = list(pass_thru) + + def __del__(self): + for widget in self.pass_thru: + widget.drag_unhighlight() + + def mask(self, widget): + """ + Remove the widget from the unmasked list. + @param widget a widget to remask + """ + assert widget in self.pass_thru, \ + "trying to mask already masked widget" + self.pass_thru.remove(widget) + widget.drag_unhighlight() + + def unmask(self, widget): + """ + Add to the unmasked list the widget passed. + A hole will be punched through the mask at that widget's position. + @param widget a widget to unmask + """ + if widget not in self.pass_thru: + widget.drag_highlight() + self.pass_thru.append(widget) + + + def set_catch_events(self, do_catch): + """Sets whether the mask catches events of widgets under it""" + if bool(self._catch_events) ^ bool(do_catch): + if do_catch: + self._catch_events = True + self.grab_add() + else: + self.grab_remove() + self._catch_events = False + + def get_catch_events(self): + """Gets whether the mask catches events of widgets under it""" + return bool(self._catch_handle) + + catch_events = property(fset=set_catch_events, fget=get_catch_events) + + def draw_with_context(self, context): + """ + Draw using the passed cairo context instead of creating a new cairo + context. This eases blending between multiple cairo-rendered + widgets. + """ + # Fill parent container + mask_alloc = self.parent.get_allocation() + oldrule = context.get_fill_rule() + context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + x, y = self.translate_coordinates(self.parent, 0, 0) + + context.rectangle(x, y, mask_alloc.width, mask_alloc.height) + for hole in self.pass_thru: + alloc = hole.get_allocation() + x, y = hole.translate_coordinates(self.parent, 0, 0) + context.rectangle(x, y, alloc.width, alloc.height) + context.set_source_rgba(0, 0, 0, 0.7) + context.fill() + context.set_fill_rule(oldrule) + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the masked widget.""" + # This is required for the event box to span across all the parent. + alloc = self.parent.get_allocation() + requisition.width = alloc.width + requisition.height = alloc.height + + def do_size_allocate(self, allocation): + """Save zone allocated to the widget.""" + self.allocation = allocation + +gobject.type_register(Mask) + + +# vim:set ts=4 sts=4 sw=4 et: diff --git a/tutorius/properties.py b/tutorius/properties.py new file mode 100644 index 0000000..34b508a --- /dev/null +++ b/tutorius/properties.py @@ -0,0 +1,323 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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 +""" +This module contains properties class that can be included in other types. +TutoriusProperties have the same behaviour as python properties (assuming you +also use the TPropContainer), with the added benefit of having builtin dialog +prompts and constraint validation. +""" + +from sugar.tutorius.constraints import Constraint, \ + UpperLimitConstraint, LowerLimitConstraint, \ + MaxSizeConstraint, MinSizeConstraint, \ + ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint + +class TPropContainer(object): + """ + A class containing properties. This does the attribute wrapping between + the container instance and the property value. As properties are on the + containing classes, they allow static introspection of those types + at the cost of needing a mapping between container instances, and + property values. This is what TPropContainer does. + """ + def __init__(self): + """ + Prepares the instance for property value storage. This is done at + object initialization, thus allowing initial mapping of properties + declared on the class. Properties won't work correctly without + this call. + """ + # create property value storage + object.__setattr__(self, "_props", {}) + for attr_name in dir(type(self)): + propinstance = object.__getattribute__(self, attr_name) + if isinstance(propinstance, TutoriusProperty): + # only care about TutoriusProperty instances + propinstance.tname = attr_name + self._props[attr_name] = propinstance.validate( + propinstance.default) + + def __getattribute__(self, name): + """ + Process the 'fake' read of properties in the appropriate instance + container. Pass 'real' attributes as usual. + """ + try: + props = object.__getattribute__(self, "_props") + except AttributeError: + # necessary for deepcopy as order of init can't be guaranteed + object.__setattr__(self, "_props", {}) + props = object.__getattribute__(self, "_props") + + try: + # try gettin value from property storage + # if it's not in the map, it's not a property or its default wasn't + # set at initialization. + return props[name] + except KeyError: + return object.__getattribute__(self, name) + + def __setattr__(self, name, value): + """ + Process the 'fake' write of properties in the appropriate instance + container. Pass 'real' attributes as usual. + + @param name the name of the property + @param value the value to assign to name + @return the setted value + """ + props = object.__getattribute__(self, "_props") + try: + # We attempt to get the property object with __getattribute__ + # to work through inheritance and benefit of the MRO. + return props.__setitem__(name, + object.__getattribute__(self, name).validate(value)) + except AttributeError: + return object.__setattr__(self, name, value) + + def get_properties(self): + """ + Return the list of property names. + """ + return object.__getattribute__(self, "_props").keys() + +class TutoriusProperty(object): + """ + The base class for all actions' properties. The interface is the following : + + value : the value of the property + + type : the type of the property + + get_contraints() : the constraints inserted on this property. They define + what is acceptable or not as values. + """ + def __init__(self): + super(TutoriusProperty, self).__init__() + self.type = None + self._constraints = None + self.default = None + + def get_constraints(self): + """ + Returns the list of constraints associated to this property. + """ + if self._constraints is None: + self._constraints = [] + for i in dir(self): + typ = getattr(self, i) + if isinstance(typ, Constraint): + self._constraints.append(i) + return self._constraints + + def validate(self, value): + """ + Validates the value of the property. If the value does not respect + the constraints on the property, this method will raise an exception. + + The exception should be of the type related to the constraint that + failed. E.g. When a int is to be set with a value that + """ + for constraint_name in self.get_constraints(): + constraint = getattr(self, constraint_name) + constraint.validate(value) + return value + +class TAddonListProperty(TutoriusProperty): + """ + Stores an addon component list as a property. + The purpose of this class is to allow correct mapping of properties + through encapsulated hierarchies. + """ + pass + + + def get_constraints(self): + """ + Returns the list of constraints associated to this property. + """ + if self._constraints is None: + self._constraints = [] + for i in dir(self): + typ = getattr(self, i) + if isinstance(typ, Constraint): + self._constraints.append(i) + return self._constraints + +class TIntProperty(TutoriusProperty): + """ + Represents an integer. Can have an upper value limit and/or a lower value + limit. + """ + + def __init__(self, value, lower_limit=None, upper_limit=None): + TutoriusProperty.__init__(self) + self.type = "int" + self.upper_limit = UpperLimitConstraint(upper_limit) + self.lower_limit = LowerLimitConstraint(lower_limit) + + self.default = self.validate(value) + +class TFloatProperty(TutoriusProperty): + """ + Represents a floating point number. Can have an upper value limit and/or + a lower value limit. + """ + def __init__(self, value, lower_limit=None, upper_limit=None): + TutoriusProperty.__init__(self) + self.type = "float" + + self.upper_limit = UpperLimitConstraint(upper_limit) + self.lower_limit = LowerLimitConstraint(lower_limit) + + self.default = self.validate(value) + +class TStringProperty(TutoriusProperty): + """ + Represents a string. Can have a maximum size limit. + """ + def __init__(self, value, size_limit=None): + TutoriusProperty.__init__(self) + self.type = "string" + self.size_limit = MaxSizeConstraint(size_limit) + + self.default = self.validate(value) + +class TArrayProperty(TutoriusProperty): + """ + Represents an array of properties. Can have a maximum number of element + limit, but there are no constraints on the content of the array. + """ + def __init__(self, value, min_size_limit=None, max_size_limit=None): + TutoriusProperty.__init__(self) + self.type = "array" + self.max_size_limit = MaxSizeConstraint(max_size_limit) + self.min_size_limit = MinSizeConstraint(min_size_limit) + self.default = self.validate(value) + +class TColorProperty(TutoriusProperty): + """ + Represents a RGB color with 3 8-bit integer values. + + The value of the property is the array [R, G, B] + """ + def __init__(self, red=None, green=None, blue=None): + TutoriusProperty.__init__(self) + self.type = "color" + + self.color_constraint = ColorConstraint() + + self._red = red or 0 + self._green = green or 0 + self._blue = blue or 0 + + self.default = self.validate([self._red, self._green, self._blue]) + +class TFileProperty(TutoriusProperty): + """ + Represents a path to a file on the disk. + """ + def __init__(self, path): + """ + Defines the path to an existing file on disk file. + + For now, the path may be relative or absolute, as long as it exists on + the local machine. + TODO : Make sure that we have a file scheme that supports distribution + on other computers (LP 355197) + """ + TutoriusProperty.__init__(self) + + self.type = "file" + + self.file_constraint = FileConstraint() + + self.default = self.validate(path) + +class TEnumProperty(TutoriusProperty): + """ + Represents a value in a given enumeration. This means that the value will + always be one in the enumeration and nothing else. + + """ + def __init__(self, value, accepted_values): + """ + Creates the enumeration property. + + @param value The initial value of the enum. Must be part of + accepted_values + @param accepted_values A list of values that the property can take + """ + TutoriusProperty.__init__(self) + + self.type = "enum" + + self.enum_constraint = EnumConstraint(accepted_values) + + self.default = self.validate(value) + +class TBooleanProperty(TutoriusProperty): + """ + Represents a True or False value. + """ + def __init__(self, value=False): + TutoriusProperty.__init__(self) + + self.type = "boolean" + + self.boolean_constraint = BooleanConstraint() + + self.default = self.validate(value) + +class TUAMProperty(TutoriusProperty): + """ + Represents a widget of the interface by storing its UAM. + """ + # TODO : Pending UAM check-in (LP 355199) + pass + +class TAddonProperty(TutoriusProperty): + """ + Reprensents an embedded tutorius Addon Component (action, trigger, etc.) + The purpose of this class is to flag the container for proper + serialization, as the contained object can't be directly dumped to text + for its attributes to be saved. + """ + class NullAction(TPropContainer): + def do(self): pass + def undo(self): pass + + def __init__(self): + super(TAddonProperty, self).__init__() + self.type = "addon" + self.default = self.NullAction() + + def validate(self, value): + if isinstance(value, TPropContainer): + return super(TAddonProperty, self).validate(value) + raise ValueError("Expected TPropContainer instance as TaddonProperty value") + +class TAddonListProperty(TutoriusProperty): + """ + Reprensents an embedded tutorius Addon List Component. + See TAddonProperty + """ + def __init__(self): + super(TAddonProperty, self).__init__() + self.type = "addonlist" + self.default = [] + + diff --git a/tutorius/services.py b/tutorius/services.py new file mode 100644 index 0000000..9ed2e50 --- /dev/null +++ b/tutorius/services.py @@ -0,0 +1,69 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 2 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 +""" +Services + +This module supplies services to be used by States, FSMs, Actions and Filters. + +Services provided are: +-Access to the running activity +-Access to the running tutorial +""" + + +class ObjectStore(object): + #Begin Singleton code + instance=None + def __new__(cls): + if not ObjectStore.instance: + ObjectStore.instance = ObjectStore.__ObjectStore() + + return ObjectStore.instance + + #End Singleton code + class __ObjectStore(object): + """ + The Object Store is a singleton class that allows access to + the current runnign activity and tutorial. + """ + def __init__(self): + self._activity = None + self._tutorial = None + #self._fsm_path = [] + + def set_activity(self, activity): + """Setter for activity""" + self._activity = activity + + def get_activity(self): + """Getter for activity""" + return self._activity + + activity = property(fset=set_activity,fget=get_activity,doc="activity") + + def set_tutorial(self, tutorial): + """Setter for tutorial""" + self._tutorial = tutorial + + def get_tutorial(self): + """Getter for tutorial""" + return self._tutorial + + tutorial = property(fset=set_tutorial,fget=get_tutorial,doc="tutorial") + + __doc__ = __ObjectStore.__doc__ + diff --git a/tutorius/testwin.py b/tutorius/testwin.py new file mode 100644 index 0000000..ef92b7f --- /dev/null +++ b/tutorius/testwin.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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 +import dragbox +import textbubble + +box = None + +def _destroy(widget, data=None): + gtk.main_quit() + +def _delete_event(widget, event, data=None): + print "quitting" + return False + +def blublu(widget, data=""): + print data + +def _drag_toggle(widget, data=None): + global box + box.dragMode = not box.dragMode + + +def addBtn(widget, data, bubble=0, btns=[0]): + if bubble == 1: + bt = textbubble.TextBubble("Bubble(%d)"%btns[0]) + else: + bt = gtk.Button("Bubble(%d)"%btns[0]) + ##bt.set_size_request(60,40) + bt.show() + data.attach(bt) + btns[0] += 1 + +def main(): + global box + win = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + win.connect("delete_event", _delete_event) + win.connect("destroy", _destroy) + + win.set_default_size(800,600) + + vbox = gtk.VBox() + vbox.show() + win.add(vbox) + + check = gtk.CheckButton(label="dragMode") + check.connect("toggled", _drag_toggle) + check.show() + vbox.pack_start(check, expand=False) + + btnadd = gtk.Button("Add Bubble") + btnadd.show() + vbox.pack_start(btnadd, expand=False) + btnadd2 = gtk.Button("Add Button") + btnadd2.show() + vbox.pack_start(btnadd2, expand=False) + +## bubble = textbubble.TextBubble("Bubbles!") +## bubble.show() +## bubble.set_size_request(40,40) +## vbox.pack_start(bubble, expand=False) + + box = dragbox.DragBox() + box.set_border_width(10) + box.show() + vbox.pack_start(box, expand=True, fill=True) + + btnadd.connect("clicked", addBtn, box, 1) + btnadd2.connect("clicked", addBtn, box) + + win.show() + gtk.main() + + +if __name__ == "__main__": + main() + diff --git a/tutorius/textbubble.py b/tutorius/textbubble.py new file mode 100644 index 0000000..e09b298 --- /dev/null +++ b/tutorius/textbubble.py @@ -0,0 +1,109 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 2 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 +""" +This module represents TextBubble widget. Also, it aims to be a short example +of drawing with Cairo. +""" + +import gtk +from math import pi as M_PI +import cairo + +# FIXME set as subclass of gtk.Widget, not EventBox +class TextBubble(gtk.EventBox): + def __init__(self, label): + gtk.EventBox.__init__(self) + + ##self.set_app_paintable(True) # else may be blank + # FIXME ensure previous call does not interfere with widget stacking + self.label = label + self.lineWidth = 5 + + self.connect("expose-event", self._on_expose) + + def __draw_with_cairo__(self, context): + """ + + """ + pass + + def _on_expose(self, widget, event): + """Redraw event callback.""" + # TODO + ctx = self.window.cairo_create() + + # set drawing region. Useless since this widget has its own window. + ##region = gtk.gdk.region_rectangle(self.allocation) + ##region.intersect(gtk.gdk.region_rectangle(event.area)) + ##ctx.region(region) + ##ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) + ##ctx.clip() + + ##import pdb; pdb.set_trace() + ##ctx.set_operator(cairo.OPERATOR_CLEAR) + ##ctx.paint() + ##ctx.set_operator(cairo.OPERATOR_OVER) + + width = self.allocation.width + height = self.allocation.height + xradius = width/2 + yradius = height/2 + width -= self.lineWidth + height -= self.lineWidth + ctx.move_to(self.lineWidth, yradius) + ctx.curve_to(self.lineWidth, self.lineWidth, + self.lineWidth, self.lineWidth, xradius, self.lineWidth) + ctx.curve_to(width, self.lineWidth, + width, self.lineWidth, width, yradius) + ctx.curve_to(width, height, width, height, xradius, height) + ctx.curve_to(self.lineWidth, height, + self.lineWidth, height, self.lineWidth, yradius) + ctx.set_source_rgb(1.0, 1.0, 1.0) + ctx.fill_preserve() + ctx.set_line_width(self.lineWidth) + ctx.set_source_rgb(0.0, 0.0, 0.0) + ctx.stroke() + + _, _, textWidth, textHeight, _, _ = ctx.text_extents(self._label) + ctx.move_to(int((self.allocation.width-textWidth)/2), + int((self.allocation.height+textHeight)/2)) + ctx.text_path(self._label) + ctx.fill() + + return True + + + def _set_label(self, value): + """Sets the label and flags the widget to be redrawn.""" + self._label = value + # FIXME hack to calculate size. necessary because may not have been + # realized + surf = cairo.SVGSurface("/dev/null", 0, 0) + ctx = cairo.Context(surf) + _, _, width, height, _, _ = ctx.text_extents(self._label) + del ctx, surf + + # FIXME bogus values follows + self.set_size_request(int(width+20), int(height+40)) + # TODO test changing a realized label + + def _get_label(self): + """Getter method for the label property""" + return self._label + + label = property(fget=_get_label, fset=_set_label,\ + doc="Text label which is to be painted on the top of the widget") + diff --git a/tutorius/uam/__init__.py b/tutorius/uam/__init__.py new file mode 100644 index 0000000..7cf5671 --- /dev/null +++ b/tutorius/uam/__init__.py @@ -0,0 +1,88 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 2 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 +""" +Universal Addressing Mechanism module + +Allows addressing Events, signals, widgets, etc for supported platforms +""" + +from urllib2 import urlparse + +import gtkparser +import gobjectparser + + +SCHEME="tap" #Tutorius Adressing Protocol + +__parsers = { + gtkparser.SCHEME:gtkparser.parse_gtk, + gobjectparser.SCHEME:gobjectparser.parse_gobject, +} + +def __add_to_urlparse(name): + #Add to uses_netloc + if not name in urlparse.uses_netloc: + urlparse.uses_netloc.append(name) + + #Add to uses_relative + if not name in urlparse.uses_relative: + urlparse.uses_relative.append(name) + +# #Add to uses_params +# if not name in urlparse.uses_params: +# urlparse.uses_params.append(name) + + #Add to uses_query + if not name in urlparse.uses_query: + urlparse.uses_query.append(name) + + #Add to uses_frament + if not name in urlparse.uses_fragment: + urlparse.uses_fragment.append(name) + + +#Add schemes to urlparse +__add_to_urlparse(SCHEME) + +for subscheme in [".".join([SCHEME,s]) for s in __parsers]: + __add_to_urlparse(subscheme) + + +class SchemeError(Exception): + def __init__(self, message): + Exception.__init__(self, message) + self.message = message + + +def parse_uri(uri): + res = urlparse.urlparse(uri) + + scheme = res.scheme.split(".")[0] + subscheme = ".".join(res.scheme.split(".")[1:]) + if not scheme == SCHEME: + raise SchemeError("Scheme %s not supported" % scheme) + + if subscheme != "" and not subscheme in __parsers: + raise SchemeError("SubScheme %s not supported" % subscheme) + + if subscheme: + return __parsers[subscheme](res) + + return res + + + diff --git a/tutorius/uam/gobjectparser.py b/tutorius/uam/gobjectparser.py new file mode 100644 index 0000000..c1fba3d --- /dev/null +++ b/tutorius/uam/gobjectparser.py @@ -0,0 +1,27 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 2 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 +""" +UAM Parser for gobject subscheme + +To be completed +""" + +SCHEME="gobject" + +def parse_gobject(parsed_uri): + """Do nothing for now""" + return parsed_uri diff --git a/tutorius/uam/gtkparser.py b/tutorius/uam/gtkparser.py new file mode 100644 index 0000000..ede2f03 --- /dev/null +++ b/tutorius/uam/gtkparser.py @@ -0,0 +1,44 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 2 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 +""" +UAM Parser for gtk subscheme + +Allows addressing Gtk Events, signals, widgets + +The gtk subscheme for tutorius is + +:///[?#] + +where: + + is the uam.SCHEME + "." + SCHEME + + is the activity's dns identifier, such as battleship.tutorius.org + + is the Hierarchical path to the widget, where 0 is the activity, such as /0/0/1/0/1/0 + + can be used to specify additionnal parameters required for an event handler or action, such as event=clicked + + must be used with params to specify which action or eventfilter to use, such as "DialogMessage" + +""" + +SCHEME="gtk" + +def parse_gtk(parsed_uri): + """Do nothing for now""" + return parsed_uri -- cgit v0.9.1