diff options
Diffstat (limited to 'tutorius')
-rw-r--r-- | tutorius/TProbe.py | 516 | ||||
-rw-r--r-- | tutorius/__init__.py | 0 | ||||
-rw-r--r-- | tutorius/actions.py | 178 | ||||
-rw-r--r-- | tutorius/addon.py | 95 | ||||
-rw-r--r-- | tutorius/constraints.py | 216 | ||||
-rw-r--r-- | tutorius/core.py | 618 | ||||
-rw-r--r-- | tutorius/creator.py | 733 | ||||
-rw-r--r-- | tutorius/dbustools.py | 42 | ||||
-rw-r--r-- | tutorius/dialog.py | 59 | ||||
-rw-r--r-- | tutorius/editor.py | 318 | ||||
-rw-r--r-- | tutorius/engine.py | 46 | ||||
-rw-r--r-- | tutorius/filters.py | 74 | ||||
-rw-r--r-- | tutorius/gtkutils.py | 203 | ||||
-rw-r--r-- | tutorius/linear_creator.py | 94 | ||||
-rw-r--r-- | tutorius/overlayer.py | 503 | ||||
-rw-r--r-- | tutorius/properties.py | 369 | ||||
-rw-r--r-- | tutorius/service.py | 85 | ||||
-rw-r--r-- | tutorius/services.py | 72 | ||||
-rw-r--r-- | tutorius/store.py | 173 | ||||
-rw-r--r-- | tutorius/testwin.py | 92 | ||||
-rw-r--r-- | tutorius/textbubble.py | 109 | ||||
-rw-r--r-- | tutorius/uam/__init__.py | 89 | ||||
-rw-r--r-- | tutorius/uam/gobjectparser.py | 27 | ||||
-rw-r--r-- | tutorius/uam/gtkparser.py | 44 | ||||
-rw-r--r-- | tutorius/vault.py | 860 | ||||
-rw-r--r-- | tutorius/viewer.py | 423 |
26 files changed, 0 insertions, 6038 deletions
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py deleted file mode 100644 index f55547c..0000000 --- a/tutorius/TProbe.py +++ /dev/null @@ -1,516 +0,0 @@ -import logging -LOGGER = logging.getLogger("sugar.tutorius.TProbe") -import os - -import gobject - -import dbus -import dbus.service -import cPickle as pickle - - -from . import addon -from .services import ObjectStore -from .properties import TPropContainer - -from .dbustools import remote_call, save_args -import copy - -""" - -------------------- - | ProbeManager | - -------------------- - | - V - -------------------- ---------- - | ProbeProxy |<---- DBus ---->| TProbe | - -------------------- ---------- - -""" -#TODO Add stub error handling for remote calls in the classes so that it will -# be clearer how errors can be handled in the future. - - -class TProbe(dbus.service.Object): - """ Tutorius Probe - Defines an entry point for Tutorius into activities that allows - performing actions and registering events onto an activity via - a DBUS Interface. - """ - - def __init__(self, activity_name, activity): - """ - Create and register a TProbe for an activity. - - @param activity_name unique activity_id - @param activity activity reference, must be a gtk container - """ - LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid()) - LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default())) - LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth())) - # Moving the ObjectStore assignment here, in the meantime - # the reference to the activity shouldn't be share as a - # global variable but passed by the Probe to the objects - # that requires it - self._activity = activity - - ObjectStore().activity = activity - - self._activity_name = activity_name - self._session_bus = dbus.SessionBus() - - # Giving a new name because _name is already used by dbus - self._name2 = dbus.service.BusName(activity_name, self._session_bus) - dbus.service.Object.__init__(self, self._session_bus, "/tutorius/Probe") - - # Add the dictionary we will use to store which actions and events - # are known - self._installedActions = {} - self._subscribedEvents = {} - - def start(self): - """ - Optional method to call if the probe is not inserted into an - existing activity. Starts a gobject mainloop - """ - mainloop = gobject.MainLoop() - print "Starting Probe for " + self._activity_name - mainloop.run() - - @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='') - def registered(self, service): - print ("Registered with: " + str(service)) - - @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='', out_signature='s') - def ping(self): - """ - Allows testing the connection to a Probe - @return string "alive" - """ - return "alive" - - # ------------------ Action handling -------------------------------------- - @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='s') - def install(self, pickled_action): - """ - Install an action on the Activity - @param pickled_action string pickled action - @return string address of installed action - """ - loaded_action = pickle.loads(str(pickled_action)) - action = addon.create(loaded_action.__class__.__name__) - - address = self._generate_action_reference(action) - - self._installedActions[address] = action - - if action._props: - action._props.update(loaded_action._props) - - action.do() - - return address - - @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='ss', out_signature='') - def update(self, address, action_props): - """ - Update an already registered action - @param address string address returned by install() - @param action_props pickled action properties - @return None - """ - action = self._installedActions[address] - - if action._props: - props = pickle.loads(str(action_props)) - action._props.update(props) - action.undo() - action.do() - - @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='') - def uninstall(self, address): - """ - Uninstall an action - @param address string address returned by install() - @return None - """ - if self._installedActions.has_key(address): - action = self._installedActions[address] - action.undo() - self._installedActions.pop(address) - - - # ------------------ Event handling --------------------------------------- - @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='s') - def subscribe(self, pickled_event): - """ - Subscribe to an Event - @param pickled_event string pickled EventFilter - @return string unique name of registered event - """ - #TODO Perform event unmapping once Tutorials use abstract events - # instead of concrete EventFilters that are tied to their - # implementation. - eventfilter = pickle.loads(str(pickled_event)) - - # The callback uses the event defined previously and each - # successive call to subscribe will register a different - # callback that references a different event - def callback(*args): - self.notify(eventfilter) - - eventfilter.install_handlers(callback, activity=self._activity) - - name = self._generate_event_reference(eventfilter) - self._subscribedEvents[name] = eventfilter - - return name - - @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='') - def unsubscribe(self, address): - """ - Remove subscription to an event - @param address string adress returned by subscribe() - @return None - """ - - if self._subscribedEvents.has_key(address): - eventfilter = self._subscribedEvents[address] - eventfilter.remove_handlers() - self._subscribedEvents.pop(address) - - @dbus.service.signal("org.tutorius.ProbeInterface") - def eventOccured(self, event): - # We need no processing now, the signal will be sent - # when the method exit - pass - - # The actual method we will call on the probe to send events - def notify(self, event): - LOGGER.debug("TProbe :: notify event %s", str(event)) - #Check that this event is even allowed - if event in self._subscribedEvents.values(): - self.eventOccured(pickle.dumps(event)) - else: - raise RuntimeWarning("Attempted to raise an unregistered event") - - # Return a unique name for this action - def _generate_action_reference(self, action): - # TODO elavoie 2009-07-25 Should return a universal address - name = action.__class__.__name__ - suffix = 1 - - while self._installedActions.has_key(name+str(suffix)): - suffix += 1 - - return name + str(suffix) - - - # Return a unique name for this event - def _generate_event_reference(self, event): - # TODO elavoie 2009-07-25 Should return a universal address - name = event.__class__.__name__ - #Keep the counter to avoid looping all the time - suffix = getattr(self, '_event_ref_suffix', 0 ) + 1 - - while self._subscribedEvents.has_key(name+str(suffix)): - suffix += 1 - - #setattr(self, '_event_ref_suffix', suffix) - - return name + str(suffix) - -class ProbeProxy: - """ - ProbeProxy is a Proxy class for connecting to a remote TProbe. - - It provides an object interface to the TProbe, which requires pickled - strings, across a DBus communication. - """ - def __init__(self, activityName): - """ - Constructor - @param activityName unique activity id. Must be a valid dbus bus name. - """ - LOGGER.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid()) - LOGGER.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default())) - LOGGER.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth())) - bus = dbus.SessionBus() - self._object = bus.get_object(activityName, "/tutorius/Probe") - self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface") - - self._actions = {} - # We keep those two data structures to be able to have multiple callbacks - # for the same event and be able to remove them independently - # _subscribedEvents holds a list of callback addresses's for each event - # _registeredCallbacks holds the functions to call for each address - self._subscribedEvents = {} - self._registeredCallbacks = {} - - - self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface") - - def _handle_signal(self, pickled_event): - event = pickle.loads(str(pickled_event)) - LOGGER.debug("ProbeProxy :: Received Event : %s %s", str(event), str(event._props.items())) - - LOGGER.debug("ProbeProxy :: Currently %d events registered", len(self._registeredCallbacks)) - if self._registeredCallbacks.has_key(event): - for callback in self._registeredCallbacks[event].values(): - callback(event) - else: - for event in self._registeredCallbacks.keys(): - LOGGER.debug("==== %s", str(event._props.items())) - LOGGER.debug("ProbeProxy :: Event does not appear to be registered") - - def isAlive(self): - try: - return self._probe.ping() == "alive" - except: - return False - - def __update_action(self, action, address): - LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address)) - self._actions[action] = str(address) - - def __clear_action(self, action): - self._actions.pop(action, None) - - def install(self, action, block=False): - """ - Install an action on the TProbe's activity - @param action Action to install - @param block Force a synchroneous dbus call if True - @return None - """ - return remote_call(self._probe.install, (pickle.dumps(action),), - save_args(self.__update_action, action), - block=block) - - def update(self, action, newaction, block=False): - """ - Update an already installed action's properties and run it again - @param action Action to update - @param newaction Action to update it with - @param block Force a synchroneous dbus call if True - @return None - """ - #TODO review how to make this work well - if not action in self._actions: - raise RuntimeWarning("Action not installed") - #TODO Check error handling - return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block) - - def uninstall(self, action, block=False): - """ - Uninstall an installed action - @param action Action to uninstall - @param block Force a synchroneous dbus call if True - """ - if action in self._actions: - remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block) - - def __update_event(self, event, callback, address): - LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) - # Since multiple callbacks could be associated to the same - # event signature, we will store multiple callbacks - # in a dictionary indexed by the unique address - # given for this subscribtion and access this - # dictionary from another one indexed by event - address = str(address) - - # We use the event object as a key - if not self._registeredCallbacks.has_key(event): - self._registeredCallbacks[event] = {} - - # TODO elavoie 2009-07-25 decide on a proper exception - # taxonomy - if self._registeredCallbacks[event].has_key(address): - # Oups, how come we have two similar addresses? - # send the bad news! - raise Exception("Probe subscribe exception, the following address already exists: " + str(address)) - - self._registeredCallbacks[event][address] = callback - - # We will keep another dictionary to remember the - # event that was associated to this unique address - # Let's copy to make sure that even if the event - # passed in is modified later it won't screw up - # our dictionary (python pass arguments by reference) - self._subscribedEvents[address] = copy.copy(event) - - return address - - def __clear_event(self, address): - LOGGER.debug("ProbeProxy :: Unregistering adress %s", str(address)) - # Cleanup everything - if self._subscribedEvents.has_key(address): - event = self._subscribedEvents[address] - - if self._registeredCallbacks.has_key(event)\ - and self._registeredCallbacks[event].has_key(address): - self._registeredCallbacks[event].pop(address) - - if self._registeredCallbacks[event] == {}: - self._registeredCallbacks.pop(event) - - self._subscribedEvents.pop(address) - else: - LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address) - - def subscribe(self, event, callback, block=True): - """ - Register an event listener - @param event Event to listen for - @param callback callable that will be called when the event occurs - @param block Force a synchroneous dbus call if True (Not allowed yet) - @return address identifier used for unsubscribing - """ - LOGGER.debug("ProbeProxy :: Registering event %s", str(hash(event))) - if not block: - raise RuntimeError("This function does not allow non-blocking mode yet") - - # TODO elavoie 2009-07-25 When we will allow for patterns both - # for event types and sources, we will need to revise the lookup - # mecanism for which callback function to call - return remote_call(self._probe.subscribe, (pickle.dumps(event),), - save_args(self.__update_event, event, callback), - block=block) - - def unsubscribe(self, address, block=True): - """ - Unregister an event listener - @param address identifier given by subscribe() - @param block Force a synchroneous dbus call if True - @return None - """ - LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address)) - if address in self._subscribedEvents.keys(): - remote_call(self._probe.unsubscribe, (address,), - return_cb=save_args(self.__clear_event, address), - block=block) - else: - LOGGER.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address) - - def detach(self, block=False): - """ - Detach the ProbeProxy from it's TProbe. All installed actions and - subscribed events should be removed. - """ - for action_addr in self._actions.keys(): - self.uninstall(action_addr, block) - - for address in self._subscribedEvents.keys(): - self.unsubscribe(address, block) - - -class ProbeManager(object): - """ - The ProbeManager provides multiplexing across multiple activity ProbeProxies - - For now, it only handles one at a time, though. - Actually it doesn't do much at all. But it keeps your encapsulation happy - """ - def __init__(self, proxy_class=ProbeProxy): - """Constructor - @param proxy_class Class to use for creating Proxies to activities. - The class should support the same interface as ProbeProxy. Exists - to make this class unit-testable by replacing the Proxy with a mock - """ - self._ProxyClass = proxy_class - self._probes = {} - self._current_activity = None - - def setCurrentActivity(self, activity_id): - if not activity_id in self._probes: - raise RuntimeError("Activity not attached") - self._current_activity = activity_id - - def getCurrentActivity(self): - return self._current_activity - - currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) - def attach(self, activity_id): - if activity_id in self._probes: - raise RuntimeWarning("Activity already attached") - - self._probes[activity_id] = self._ProxyClass(activity_id) - #TODO what do we do with this? Raise something? - if self._probes[activity_id].isAlive(): - print "Alive!" - else: - print "FAil!" - - def detach(self, activity_id): - if activity_id in self._probes: - probe = self._probes.pop(activity_id) - probe.detach() - if self._current_activity == activity_id: - self._current_activity = None - - def install(self, action, block=False): - """ - Install an action on the current activity - @param action Action to install - @param block Force a synchroneous dbus call if True - @return None - """ - if self.currentActivity: - return self._probes[self.currentActivity].install(action, block) - else: - raise RuntimeWarning("No activity attached") - - def update(self, action, newaction, block=False): - """ - Update an already installed action's properties and run it again - @param action Action to update - @param newaction Action to update it with - @param block Force a synchroneous dbus call if True - @return None - """ - if self.currentActivity: - return self._probes[self.currentActivity].update(action, newaction, block) - else: - raise RuntimeWarning("No activity attached") - - def uninstall(self, action, block=False): - """ - Uninstall an installed action - @param action Action to uninstall - @param block Force a synchroneous dbus call if True - """ - if self.currentActivity: - return self._probes[self.currentActivity].uninstall(action, block) - else: - raise RuntimeWarning("No activity attached") - - def subscribe(self, event, callback): - """ - Register an event listener - @param event Event to listen for - @param callback callable that will be called when the event occurs - @return address identifier used for unsubscribing - """ - if self.currentActivity: - return self._probes[self.currentActivity].subscribe(event, callback) - else: - raise RuntimeWarning("No activity attached") - - def unsubscribe(self, address): - """ - Unregister an event listener - @param address identifier given by subscribe() - @return None - """ - if self.currentActivity: - return self._probes[self.currentActivity].unsubscribe(address) - else: - raise RuntimeWarning("No activity attached") - diff --git a/tutorius/__init__.py b/tutorius/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/tutorius/__init__.py +++ /dev/null diff --git a/tutorius/actions.py b/tutorius/actions.py deleted file mode 100644 index bb15459..0000000 --- a/tutorius/actions.py +++ /dev/null @@ -1,178 +0,0 @@ -# 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 -""" -import gtk - -from gettext import gettext as _ - -from sugar.graphics import icon - -from . import addon -from .services import ObjectStore -from .properties import * - -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() - diff --git a/tutorius/addon.py b/tutorius/addon.py deleted file mode 100644 index 7ac68f7..0000000 --- a/tutorius/addon.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Simon Poirier <simpoir@gmail.com> -# -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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" - -TYPE_ACTION = 'action' -TYPE_EVENT = 'event' - -_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__ - mod.__action__['type'] = TYPE_ACTION - continue - if hasattr(mod, "__event__"): - _cache[mod.__event__['name']] = mod.__event__ - mod.__event__['type'] = TYPE_EVENT - -def create(name, *args, **kwargs): - global _cache - if not _cache: - _reload_addons() - try: - comp_metadata = _cache[name] - try: - return comp_metadata['class'](*args, **kwargs) - except: - logging.error("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs))) - return None - 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] - -def get_name_from_type(typ): - global _cache - if not _cache: - _reload_addons() - for addon in _cache.keys(): - if typ == _cache[addon]['class']: - return addon - return None - -# vim:set ts=4 sts=4 sw=4 et: diff --git a/tutorius/constraints.py b/tutorius/constraints.py deleted file mode 100644 index 519bce8..0000000 --- a/tutorius/constraints.py +++ /dev/null @@ -1,216 +0,0 @@ -# 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 ConstraintException(Exception): - """ - Parent class for all constraint exceptions - """ - pass - -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(ConstraintException): - 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(ConstraintException): - 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(ConstraintException): - 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(ConstraintException): - 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(ConstraintException): - 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(ConstraintException): - 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(ConstraintException): - 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(ConstraintException): - 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? - # FIXME This is a hack to make cases where a default file is not valid - # work. It allows None values to be validated, though - if value is None: - return - if not os.path.isfile(value): - raise FileConstraintError("Non-existing file : %s"%value) - return - diff --git a/tutorius/core.py b/tutorius/core.py deleted file mode 100644 index bfbe07b..0000000 --- a/tutorius/core.py +++ /dev/null @@ -1,618 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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 logging -import os - -from .TProbe import ProbeManager -from .dbustools import save_args -from . import addon - -logger = logging.getLogger("tutorius") - -class Tutorial (object): - """ - Tutorial Class, used to run through the FSM. - """ - #Properties - probeManager = property(lambda self: self._probeMgr) - activityId = property(lambda self: self._activity_id) - - 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._probeMgr = ProbeManager() - self._activity_id = None - #Rest of initialisation happens when attached - - def attach(self, activity_id): - """ - Attach to a running activity - - @param activity_id the id of the activity to attach to - """ - #For now, absolutely detach if a previous one! - if self._activity_id: - self.detach() - self._activity_id = activity_id - self._probeMgr.attach(activity_id) - self._probeMgr.currentActivity = activity_id - 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() - - if not self._activity_id is None: - self._probeMgr.detach(self._activity_id) - self._activity_id = None - - def set_state(self, name): - """ - Switch to a new state - """ - logger.debug("==== NEW STATE: %s ====" % name) - - self.state_machine.set_state(name) - - 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 - readfile = addon.create("ReadFile", filename=filename) - if readfile: - self._probeMgr.install(readfile) - #Uninstall now while we have the reference handy - self._probeMgr.uninstall(readfile) - -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 [] - - self._transitions= dict(event_filter_list or []) - - self._installedEvents = set() - - 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 (event, next_state) in self._transitions.items(): - self._installedEvents.add(self.tutorial.probeManager.subscribe(event, save_args(self._event_filter_state_done_cb, next_state ))) - - for action in self._actions: - self.tutorial.probeManager.install(action) - - 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 - while len(self._installedEvents) > 0: - self.tutorial.probeManager.unsubscribe(self._installedEvents.pop()) - - # Undo all the actions related to this state - for action in self._actions: - self.tutorial.probeManager.uninstall(action) - - def _event_filter_state_done_cb(self, next_state, event): - """ - 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 next_state The next state for the transition - @param event The event that occured - """ - # Run the tests here, if need be - - # Warn the higher level that we wish to change state - self.tutorial.set_state(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 - - @param new_action The new action to execute when in this state - @return True if added, False otherwise - """ - self._actions.append(new_action) - return True - - # 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. - """ - #FIXME What if the action is currently installed? - self._actions = [] - - def add_event_filter(self, event, next_state): - """ - Adds an event filter that will cause a transition from this state. - - The same event filter may not be added twice. - - @param event The event that will trigger a transition - @param next_state The state to which the transition will lead - @return True if added, False otherwise - """ - if event not in self._transitions.keys(): - self._transitions[event]=next_state - return True - return False - - def get_event_filter_list(self): - """ - @return The list of event filters associated with this state. - """ - return self._transitions.items() - - 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._transitions = {} - - def __eq__(self, otherState): - """ - Compares two states and tells whether they contain the same states with the - same actions and event filters. - - @param otherState The other State that we wish to match - @returns True if every action in this state has a matching action in the - other state with the same properties and values AND if every - event filters in this state has a matching filter in the - other state having the same properties and values AND if both - states have the same name. -` """ - if not isinstance(otherState, State): - return False - if self.name != otherState.name: - return False - - # Do they have the same actions? - if len(self._actions) != len(otherState._actions): - return False - - if len(self._transitions) != len(otherState._transitions): - return False - - for act in self._actions: - found = False - # For each action in the other state, try to match it with this one. - for otherAct in otherState._actions: - if act == otherAct: - found = True - break - if found == False: - # If we arrive here, then we could not find an action with the - # same values in the other state. We know they're not identical - return False - - # Do they have the same event filters? - if self._transitions != otherState._transitions: - return False - - # If nothing failed up to now, then every actions and every filters can - # be found in the other state - return True - -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: - self.tutorial.probeManager.install(action) - - # 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: - self.tutorial.probeManager.uninstall(action) - - # 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 in st._transitions: - if st._transitions[event] == state_name: - del st._transitions[event] - - # 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, state in state._transitions.items(): - next_states.add(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, state in st._transitions.items(): - if state == state_name: - states.append(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 - - def __eq__(self, otherFSM): - """ - Compares the elements of two FSM to ensure and returns true if they have the - same set of states, containing the same actions and the same event filters. - - @returns True if the two FSMs have the same content, False otherwise - """ - if not isinstance(otherFSM, FiniteStateMachine): - return False - - # Make sure they share the same name - if not (self.name == otherFSM.name) or \ - not (self.start_state_name == otherFSM.start_state_name): - return False - - # Ensure they have the same number of FSM-level actions - if len(self._actions) != len(otherFSM._actions): - return False - - # Test that we have all the same FSM level actions - for act in self._actions: - found = False - # For every action in the other FSM, try to match it with the - # current one. - for otherAct in otherFSM._actions: - if act == otherAct: - found = True - break - if found == False: - return False - - # Make sure we have the same number of states in both FSMs - if len(self._states) != len(otherFSM._states): - return False - - # For each state, try to find a corresponding state in the other FSM - for state_name in self._states.keys(): - state = self._states[state_name] - other_state = None - try: - # Attempt to use this key in the other FSM. If it's not present - # the dictionary will throw an exception and we'll know we have - # at least one different state in the other FSM - other_state = otherFSM._states[state_name] - except: - return False - # If two states with the same name exist, then we want to make sure - # they are also identical - if not state == other_state: - return False - - # If we made it here, then all the states in this FSM could be matched to an - # identical state in the other FSM. - return True diff --git a/tutorius/creator.py b/tutorius/creator.py deleted file mode 100644 index c477056..0000000 --- a/tutorius/creator.py +++ /dev/null @@ -1,733 +0,0 @@ -""" -This package contains UI classes related to tutorial authoring. -This includes visual display of tools to edit and create tutorials from within -the activity itself. -""" -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Simon Poirier <simpoir@gmail.com> -# -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -import gtk.gdk -import gtk.glade -import gobject -from gettext import gettext as T - -import os -from sugar.graphics import icon -import copy - -from . import overlayer, gtkutils, actions, vault, properties, addon -from . import filters -from .services import ObjectStore -from .core import Tutorial, FiniteStateMachine, State -from . import viewer - -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 = FiniteStateMachine('Untitled') - self._state = State(name='INIT') - self._tutorial.add_state(self._state) - self._state_counter = 1 - else: - self._tutorial = tutorial - # TODO load existing tutorial; unused yet - - self._action_panel = None - self._current_filter = None - self._intro_mask = None - self._intro_handle = None - allocation = self._activity.get_allocation() - self._width = allocation.width - self._height = allocation.height - self._selected_widget = None - self._eventmenu = None - self.tuto = None - self._guid = None - - self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5)) - self._activity._overlayer.put(self._hlmask, 0, 0) - - dlg_width = 300 - dlg_height = 70 - sw = gtk.gdk.screen_width() - sh = gtk.gdk.screen_height() - - self._propedit = ToolBox(self._activity) - self._propedit.tree.signal_autoconnect({ - 'on_quit_clicked': self._cleanup_cb, - 'on_save_clicked': self.save, - 'on_action_activate': self._add_action_cb, - 'on_event_activate': self._add_event_cb, - }) - self._propedit.window.move( - gtk.gdk.screen_width()-self._propedit.window.get_allocation().width, - 100) - - - self._overview = viewer.Viewer(self._tutorial, self) - self._overview.win.set_transient_for(self._activity) - - self._overview.win.move(0, gtk.gdk.screen_height()- \ - self._overview.win.get_allocation().height) - - self._transitions = dict() - - def _update_next_state(self, state, event, next_state): - self._transitions[event] = next_state - - evts = state.get_event_filter_list() - state.clear_event_filters() - for evt, next_state in evts: - state.add_event_filter(evt, self._transitions[evt]) - - def delete_action(self, action): - """ - Removes the first instance of specified action from the tutorial. - - @param action: the action object to remove from the tutorial - @returns: True if successful, otherwise False. - """ - state = self._tutorial.get_state_by_name("INIT") - - while True: - state_actions = state.get_action_list() - for fsm_action in state_actions: - if fsm_action is action: - state.clear_actions() - if state is self._state: - fsm_action.exit_editmode() - state_actions.remove(fsm_action) - self.set_insertion_point(state.name) - for keep_action in state_actions: - state.add_action(keep_action) - return True - - ev_list = state.get_event_filter_list() - if ev_list: - state = self._tutorial.get_state_by_name(ev_list[0][1]) - continue - - return False - - def delete_state(self): - """ - Remove current state. - Limitation: The last state cannot be removed, as it doesn't have - any transitions to remove anyway. - - @returns: True if successful, otherwise False. - """ - if not self._state.get_event_filter_list(): - # last state cannot be removed - return False - - state = self._tutorial.get_state_by_name("INIT") - ev_list = state.get_event_filter_list() - if state is self._state: - next_state = self._tutorial.get_state_by_name(ev_list[0][1]) - self.set_insertion_point(next_state.name) - self._tutorial.remove_state(state.name) - self._tutorial.remove_state(next_state.name) - next_state.name = "INIT" - self._tutorial.add_state(next_state) - return True - - # loop to repair links from deleted state - while ev_list: - next_state = self._tutorial.get_state_by_name(ev_list[0][1]) - if next_state is self._state: - # the tutorial will flush the event filters. We'll need to - # clear and re-add them. - self._tutorial.remove_state(self._state.name) - state.clear_event_filters() - self._update_next_state(state, ev_list[0][0], next_state.get_event_filter_list()[0][1]) - for ev, next_state in ev_list: - state.add_event_filter(ev, next_state) - - self.set_insertion_point(ev_list[0][1]) - return True - - state = next_state - ev_list = state.get_event_filter_list() - return False - - def get_insertion_point(self): - return self._state.name - - def set_insertion_point(self, state_name): - for action in self._state.get_action_list(): - action.exit_editmode() - self._state = self._tutorial.get_state_by_name(state_name) - self._overview.win.queue_draw() - state_actions = self._state.get_action_list() - for action in state_actions: - action.enter_editmode() - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) - - if state_actions: - self._propedit.action = state_actions[0] - else: - self._propedit.action = None - - - def _evfilt_cb(self, menuitem, event): - """ - This will get called once the user has selected a menu item from the - event filter popup menu. This should add the correct event filter - to the FSM and increment states. - """ - # undo actions so they don't persist through step editing - for action in self._state.get_action_list(): - action.exit_editmode() - 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 _add_action_cb(self, widget, path): - """Callback for the action creation toolbar tool""" - action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME] - action = addon.create(action_type) - action.enter_editmode() - self._state.add_action(action) - # FIXME: replace following with event catching - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) - self._overview.win.queue_draw() - - def _add_event_cb(self, widget, path): - """Callback for the event creation toolbar tool""" - event_type = self._propedit.events_list[path][ToolBox.ICON_NAME] - event = addon.create(event_type) - addonname = type(event).__name__ - meta = addon.get_addon_meta(addonname) - for propname in meta['mandatory_props']: - prop = getattr(type(event), propname) - if isinstance(prop, properties.TUAMProperty): - selector = WidgetSelector(self._activity) - setattr(event, propname, selector.select()) - elif isinstance(prop, properties.TEventType): - try: - dlg = SignalInputDialog(self._activity, - text="Mandatory property", - field=propname, - addr=event.object_id) - setattr(event, propname, dlg.pop()) - except AttributeError: - pass - elif isinstance(prop, properties.TStringProperty): - dlg = TextInputDialog(self._activity, - text="Mandatory property", - field=propname) - setattr(event, propname, dlg.pop()) - else: - raise NotImplementedError() - - event_filters = self._state.get_event_filter_list() - if event_filters: - # linearize tutorial by inserting state - new_state = State(name=str(self._state_counter)) - self._state_counter += 1 - self._state.clear_event_filters() - for evt_filt, next_state in event_filters: - new_state.add_event_filter(evt_filt, next_state) - self._update_next_state(self._state, event, new_state.name) - next_state = new_state.name - # blocks are shifted, full redraw is necessary - self._overview.win.queue_draw() - else: - # append empty state only if edit inserting at end of linearized - # tutorial. - self._update_next_state(self._state, event, str(self._state_counter)) - next_state = str(self._state_counter) - new_state = State(name=str(self._state_counter)) - self._state_counter += 1 - - self._state.add_event_filter(event, next_state) - self._tutorial.add_state(new_state) - self._overview.win.queue_draw() - - self.set_insertion_point(new_state.name) - - 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 - - self._overview.win.queue_draw() - - def _cleanup_cb(self, *args): - """ - Quit editing and cleanup interface artifacts. - """ - # undo actions so they don't persist through step editing - for action in self._state.get_action_list(): - action.exit_editmode() - - dialog = gtk.MessageDialog( - parent=self._activity, - flags=gtk.DIALOG_MODAL, - type=gtk.MESSAGE_QUESTION, - buttons=gtk.BUTTONS_YES_NO, - message_format=T('Do you want to save before stopping edition?')) - do_save = dialog.run() - dialog.destroy() - if do_save == gtk.RESPONSE_YES: - self.save() - - # remove UI remains - self._hlmask.covered = None - self._activity._overlayer.remove(self._hlmask) - self._hlmask.destroy() - self._hlmask = None - self._propedit.destroy() - self._overview.destroy() - self._activity.queue_draw() - del self._activity._creator - - def save(self, widget=None): - if not self.tuto: - dlg = TextInputDialog(self._activity, - text=T("Enter a tutorial title."), - field=T("Title")) - tutorialName = "" - while not tutorialName: tutorialName = dlg.pop() - dlg.destroy() - - # prepare tutorial for serialization - self.tuto = Tutorial(tutorialName, self._tutorial) - bundle = vault.TutorialBundler(self._guid) - self._guid = bundle.Guid - bundle.write_metadata_file(self.tuto) - bundle.write_fsm(self._tutorial) - - - 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 ToolBox(object): - ICON_LABEL = 0 - ICON_IMAGE = 1 - ICON_NAME = 2 - ICON_TIP = 3 - def __init__(self, parent): - super(ToolBox, self).__init__() - self.__parent = parent - sugar_prefix = os.getenv("SUGAR_PREFIX",default="/usr") - glade_file = os.path.join(sugar_prefix, 'share', 'tutorius', - 'ui', 'creator.glade') - self.tree = gtk.glade.XML(glade_file) - self.window = self.tree.get_widget('mainwindow') - self._propbox = self.tree.get_widget('propbox') - - self.window.set_transient_for(parent) - - self._action = None - self.actions_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str) - self.actions_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING) - self.events_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str) - self.events_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING) - - for toolname in addon.list_addons(): - meta = addon.get_addon_meta(toolname) - iconfile = gtk.Image() - iconfile.set_from_file(icon.get_icon_file_name(meta['icon'])) - img = iconfile.get_pixbuf() - label = format_multiline(meta['display_name']) - - if meta['type'] == addon.TYPE_ACTION: - self.actions_list.append((label, img, toolname, meta['display_name'])) - else: - self.events_list.append((label, img, toolname, meta['display_name'])) - - iconview_action = self.tree.get_widget('iconview1') - iconview_action.set_model(self.actions_list) - iconview_action.set_text_column(self.ICON_LABEL) - iconview_action.set_pixbuf_column(self.ICON_IMAGE) - iconview_action.set_tooltip_column(self.ICON_TIP) - iconview_event = self.tree.get_widget('iconview2') - iconview_event.set_model(self.events_list) - iconview_event.set_text_column(self.ICON_LABEL) - iconview_event.set_pixbuf_column(self.ICON_IMAGE) - iconview_event.set_tooltip_column(self.ICON_TIP) - - self.window.show() - - def destroy(self): - """ clean and free the toolbox """ - self.window.destroy() - - def refresh_properties(self): - """Refresh property values from the selected action.""" - if self._action is None: - return - 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.TUAMProperty): - propwdg = row.get_children()[1] - propwdg.set_label(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_properties() - return - for old_prop in self._propbox.get_children(): - self._propbox.remove(old_prop) - - 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.TUAMProperty): - propwdg = gtk.Button(propval) - propwdg.connect_after("clicked", \ - self._uam_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._propbox.show_all() - self.refresh_properties() - - def get_action(self): - """Getter for the action property""" - return self._action - action = property(fset=set_action, fget=get_action, doc=\ - "Action to be edited through introspection.") - - def _list_prop_changed(self, widget, evt, action, propname, idx): - try: - #Save props as tuples so that they can be hashed - attr = list(getattr(action, propname)) - attr[idx] = int(widget.get_text()) - setattr(action, propname, tuple(attr)) - except ValueError: - widget.set_text(str(getattr(action, propname)[idx])) - self.__parent._creator._action_refresh_cb(None, None, action) - def _uam_prop_changed(self, widget, action, propname): - selector = WidgetSelector(self.__parent) - selection = selector.select() - setattr(action, propname, selection) - 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 WidgetSelector(object): - """ - Allow selecting a widget from within a window without interrupting the - flow of the current call. - - The selector will run on the specified window until either a widget - is selected or abort() gets called. - """ - def __init__(self, window): - super(WidgetSelector, self).__init__() - self.window = window - self._intro_mask = None - self._intro_handle = None - self._select_handle = None - self._prelight = None - - def select(self): - """ - Starts selecting a widget, by grabbing control of the mouse and - highlighting hovered widgets until one is clicked. - @returns: a widget address or None - """ - if not self._intro_mask: - self._prelight = None - self._intro_mask = overlayer.Mask(catch_events=True) - self._select_handle = self._intro_mask.connect_after( - "button-press-event", self._end_introspect) - self._intro_handle = self._intro_mask.connect_after( - "motion-notify-event", self._intro_cb) - self.window._overlayer.put(self._intro_mask, 0, 0) - self.window._overlayer.queue_draw() - - while bool(self._intro_mask) and not gtk.main_iteration(): - pass - - return gtkutils.raddr_lookup(self._prelight) - - def _end_introspect(self, widget, evt): - if evt.type == gtk.gdk.BUTTON_PRESS and self._prelight: - self._intro_mask.catch_events = False - self._intro_mask.disconnect(self._intro_handle) - self._intro_handle = None - self._intro_mask.disconnect(self._select_handle) - self._select_handle = None - self.window._overlayer.remove(self._intro_mask) - self._intro_mask = None - # for some reason, gtk may not redraw after this unless told to. - self.window.queue_draw() - - def _intro_cb(self, widget, evt): - """ - Callback for capture of widget events, when in introspect mode. - """ - # widget has focus, let's hilight it - win = gtk.gdk.display_get_default().get_window_at_pointer() - if not win: - return - click_wdg = win[0].get_user_data() - if not click_wdg.is_ancestor(self.window._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._prelight = click_wdg - - self.window.queue_draw() - - def abort(self): - """ - Ends the selection. The control will return to the select() caller - with a return value of None, as selection was aborted. - """ - self._intro_mask.catch_events = False - self._intro_mask.disconnect(self._intro_handle) - self._intro_handle = None - self._intro_mask.disconnect(self._select_handle) - self._select_handle = None - self.window._overlayer.remove(self._intro_mask) - self._intro_mask = None - self._prelight = None - -class SignalInputDialog(gtk.MessageDialog): - def __init__(self, parent, text, field, addr): - """ - Create a gtk signal selection dialog. - - @param parent: the parent window this dialog should stay over. - @param text: the title of the dialog. - @param field: the field description of the dialog. - @param addr: the widget address from which to fetch signal list. - """ - gtk.MessageDialog.__init__(self, parent, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_QUESTION, - gtk.BUTTONS_OK, - None) - self.set_markup(text) - self.model = gtk.ListStore(str) - widget = gtkutils.find_widget(parent, addr) - for signal_name in gobject.signal_list_names(widget): - self.model.append(row=(signal_name,)) - self.entry = gtk.ComboBox(self.model) - cell = gtk.CellRendererText() - self.entry.pack_start(cell) - self.entry.add_attribute(cell, 'text', 0) - 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): - """ - Show the dialog. It will run in it's own loop and return control - to the caller when a signal has been selected. - - @returns: a signal name or None if no signal was selected - """ - self.run() - self.hide() - iter = self.entry.get_active_iter() - if iter: - text = self.model.get_value(iter, 0) - return text - return None - - def _dialog_done_cb(self, entry, response): - self.response(response) - -class TextInputDialog(gtk.MessageDialog): - def __init__(self, parent, text, field): - gtk.MessageDialog.__init__(self, parent, - 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) - -# The purpose of this function is to reformat text, as current IconView -# implentation does not insert carriage returns on long lines. -# To preserve layout, this call reformat text to fit in small space under an -# icon. -def format_multiline(text, length=10, lines=3, line_separator='\n'): - """ - Reformat a text to fit in a small space. - - @param length: maximum char per line - @param lines: maximum number of lines - """ - words = text.split(' ') - line = list() - return_val = [] - linelen = 0 - - for word in words: - t_len = linelen+len(word) - if t_len < length: - line.append(word) - linelen = t_len+1 # count space - else: - if len(return_val)+1 < lines: - return_val.append(' '.join(line)) - line = list() - linelen = 0 - line.append(word) - else: - return_val.append(' '.join(line+['...'])) - return line_separator.join(return_val) - - return_val.append(' '.join(line)) - return line_separator.join(return_val) - - -# vim:set ts=4 sts=4 sw=4 et: diff --git a/tutorius/dbustools.py b/tutorius/dbustools.py deleted file mode 100644 index 5d70d7b..0000000 --- a/tutorius/dbustools.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -LOGGER = logging.getLogger("sugar.tutorius.dbustools") - -def save_args(callable, *xargs, **xkwargs): - def __call(*args, **kwargs): - kw = dict() - kw.update(kwargs) - kw.update(xkwargs) - return callable(*(xargs+args), **kw) - return __call - -def ignore(*args): - LOGGER.debug("Unhandled asynchronous dbus call response with arguments: %s", str(args)) - -def logError(error): - LOGGER.error("Unhandled asynchronous dbus call error: %s", error) - -def remote_call(callable, args, return_cb=None, error_cb=None, block=False): - reply_cb = return_cb or ignore - errhandler_cb = error_cb or logError - if block: - try: - ret_val = callable(*args) - LOGGER.debug("remote_call return arguments: %s", str(ret_val)) - except Exception, e: - #Use the specified error handler even for blocking calls - errhandler_cb(e) - return - - #Return value signature might be : - if ret_val is None: - #Nothing - return reply_cb() - elif type(ret_val) in (list, tuple): - #Several parameters - return reply_cb(*ret_val) - else: - #One parameter - return reply_cb(ret_val) - else: - callable(*args, reply_handler=reply_cb, error_handler=errhandler_cb) - diff --git a/tutorius/dialog.py b/tutorius/dialog.py deleted file mode 100644 index be51a0e..0000000 --- a/tutorius/dialog.py +++ /dev/null @@ -1,59 +0,0 @@ -# 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 deleted file mode 100644 index 9d2effe..0000000 --- a/tutorius/editor.py +++ /dev/null @@ -1,318 +0,0 @@ -# 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 .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/engine.py b/tutorius/engine.py deleted file mode 100644 index e77a018..0000000 --- a/tutorius/engine.py +++ /dev/null @@ -1,46 +0,0 @@ -import logging -import dbus.mainloop.glib -from jarabe.model import shell -from sugar.bundle.activitybundle import ActivityBundle - -from .vault import Vault - -class Engine: - """ - Driver for the execution of tutorials - """ - - def __init__(self): - # FIXME Probe management should be in the probe manager - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - #FIXME shell.get_model() will only be useful in the shell process - self._shell = shell.get_model() - self._tutorial = None - - def launch(self, tutorialID): - """ Launch a tutorial - @param tutorialID unique tutorial identifier used to retrieve it from the disk - """ - if self._tutorial: - self._tutorial.detach() - self._tutorial = None - - #Get the active activity from the shell - activity = self._shell.get_active_activity() - self._tutorial = Vault.loadTutorial(tutorialID) - - #TProbes automatically use the bundle id, available from the ActivityBundle - bundle = ActivityBundle(activity.get_bundle_path()) - self._tutorial.attach(bundle.get_bundle_id()) - - def stop(self): - """ Stop the current tutorial - """ - self._tutorial.detach() - self._tutorial = None - - def pause(self): - """ Interrupt the current tutorial and save its state in the journal - """ - raise NotImplementedError("Unable to store tutorial state") - diff --git a/tutorius/filters.py b/tutorius/filters.py deleted file mode 100644 index 38cf86b..0000000 --- a/tutorius/filters.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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 logging -logger = logging.getLogger("filters") - -from . import properties - - -class EventFilter(properties.TPropContainer): - """ - Base class for an event filter - """ - - def __init__(self): - """ - Constructor. - """ - super(EventFilter, self).__init__() - self._callback = None - - 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) - diff --git a/tutorius/gtkutils.py b/tutorius/gtkutils.py deleted file mode 100644 index 1a9cb0f..0000000 --- a/tutorius/gtkutils.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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 deleted file mode 100644 index f664c49..0000000 --- a/tutorius/linear_creator.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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 copy import deepcopy - -from .core import * -from .actions import * -from .filters import * - -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) - state = State(self.state_name, action_list=self.current_actions, - event_filter_list=[(event_filter, next_state_name),]) - 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 deleted file mode 100644 index b967739..0000000 --- a/tutorius/overlayer.py +++ /dev/null @@ -1,503 +0,0 @@ -""" -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): - super(Overlayer, self).__init__() - - self._overlayed = overlayed - if overlayed: - self.put(overlayed, 0, 0) - - self.__realizer = self.connect_after("realize", 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 - super(Overlayer, self).put(child, x, y) - - # be sure to redraw or the overlay may not show - self.queue_draw() - - - def __init_realized(self, widget): - """ - 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 = "<b>%s</b>"%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 deleted file mode 100644 index 5422532..0000000 --- a/tutorius/properties.py +++ /dev/null @@ -1,369 +0,0 @@ -# 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 copy import copy, deepcopy - -from .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( - copy(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() - - def get_properties_dict_copy(self): - """ - Return a deep copy of the dictionary of properties from that object. - """ - return deepcopy(self._props) - - # Providing the hash methods necessary to use TPropContainers - # in a dictionary, according to their properties - def __hash__(self): - #Return a hash of properties (key, value) sorted by key - #We need to transform the list of property key, value lists into - # a tuple of key, value tuples - return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0]))))) - - def __eq__(self, e2): - return isinstance(e2, type(self)) and self._props == e2._props - - # Adding methods for pickling and unpickling an object with - # properties - def __getstate__(self): - return self._props.copy() - - def __setstate__(self, dict): - self._props.update(dict) - -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 - -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 = tuple(self.validate(value)) - - #Make this thing hashable - def __setstate__(self, state): - self.max_size_limit = MaxSizeConstraint(state["max_size_limit"]) - self.min_size_limit = MinSizeConstraint(state["min_size_limit"]) - self.value = state["value"] - - def __getstate__(self): - return dict( - max_size_limit=self.max_size_limit.limit, - min_size_limit=self.min_size_limit.limit, - value=self.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. - """ - def __init__(self, value=None): - TutoriusProperty.__init__(self) - - self.type = "uam" - - self.default = self.validate(value) - -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 TEventType(TutoriusProperty): - """ - Represents an GUI signal for a widget. - """ - def __init__(self, value): - super(TEventType, self).__init__() - self.type = "gtk-signal" - - self.default = self.validate(value) - -class TAddonListProperty(TutoriusProperty): - """ - Reprensents an embedded tutorius Addon List Component. - See TAddonProperty - """ - def __init__(self): - TutoriusProperty.__init__(self) - self.type = "addonlist" - self.default = [] - - def validate(self, value): - if isinstance(value, list): - for component in value: - if not (isinstance(component, TPropContainer)): - raise ValueError("Expected a list of TPropContainer instances inside TAddonListProperty value, got a %s" % (str(type(component)))) - return value - raise ValueError("Value proposed to TAddonListProperty is not a list") - diff --git a/tutorius/service.py b/tutorius/service.py deleted file mode 100644 index eb246a1..0000000 --- a/tutorius/service.py +++ /dev/null @@ -1,85 +0,0 @@ -import dbus - -from .engine import Engine -from .dbustools import remote_call - -_DBUS_SERVICE = "org.tutorius.Service" -_DBUS_PATH = "/org/tutorius/Service" -_DBUS_SERVICE_IFACE = "org.tutorius.Service" - -class Service(dbus.service.Object): - """ - Global tutorius entry point to control the whole system - """ - - def __init__(self): - bus = dbus.SessionBus() - bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus) - dbus.service.Object.__init__(self, bus_name, _DBUS_PATH) - - self._engine = None - - def start(self): - """ Start the service itself - """ - # For the moment there is nothing to do - pass - - - @dbus.service.method(_DBUS_SERVICE_IFACE, - in_signature="s", out_signature="") - def launch(self, tutorialID): - """ Launch a tutorial - @param tutorialID unique tutorial identifier used to retrieve it from the disk - """ - if self._engine == None: - self._engine = Engine() - self._engine.launch(tutorialID) - - @dbus.service.method(_DBUS_SERVICE_IFACE, - in_signature="", out_signature="") - def stop(self): - """ Stop the current tutorial - """ - self._engine.stop() - - @dbus.service.method(_DBUS_SERVICE_IFACE, - in_signature="", out_signature="") - def pause(self): - """ Interrupt the current tutorial and save its state in the journal - """ - self._engine.pause() - -class ServiceProxy: - """ Proxy to connect to the Service object, abstracting the DBus interface""" - - def __init__(self): - bus = dbus.SessionBus() - self._object = bus.get_object(_DBUS_SERVICE,_DBUS_PATH) - self._service = dbus.Interface(self._object, _DBUS_SERVICE_IFACE) - - def launch(self, tutorialID): - """ Launch a tutorial - @param tutorialID unique tutorial identifier used to retrieve it from the disk - """ - remote_call(self._service.launch, (tutorialID, ), block=False) - - def stop(self): - """ Stop the current tutorial - """ - remote_call(self._service.stop, (), block=False) - - def pause(self): - """ Interrupt the current tutorial and save its state in the journal - """ - remote_call(self._service.pause, (), block=False) - -if __name__ == "__main__": - import dbus.mainloop.glib - import gobject - - loop = gobject.MainLoop() - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - s = Service() - loop.run() - diff --git a/tutorius/services.py b/tutorius/services.py deleted file mode 100644 index e7b17d8..0000000 --- a/tutorius/services.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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 - -TODO: Passing the activity reference should be done by the Probe instead -of being a global variable. -""" - - -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/store.py b/tutorius/store.py deleted file mode 100644 index 9c8bdff..0000000 --- a/tutorius/store.py +++ /dev/null @@ -1,173 +0,0 @@ -# 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 urllib - -class StoreProxy(object): - """ - Implements a communication channel with the Tutorius Store, where tutorials - are shared from around the world. This proxy is meant to offer a one-stop - shop to implement all the requests that could be made to the Store. - """ - - def get_categories(self): - """ - Returns all the categories registered in the store. Categories are used to - classify tutorials according to a theme. (e.g. Mathematics, History, etc...) - - @return The list of category names stored on the server. - """ - raise NotImplementedError("get_categories() not implemented") - - def get_tutorials(self, keywords=None, category=None, startIndex=0, numResults=10, sortBy='name'): - """ - Returns the list of tutorials that correspond to the given search criteria. - - @param keywords The list of keywords that should be matched inside the tutorial title - or description. If None, the search will not filter the results - according to the keywords. - @param category The category in which to restrict the search. - @param startIndex The index in the result set from which to return results. This is - used to allow applications to fetch results one set at a time. - @param numResults The max number of results that can be returned - @param sortBy The field on which to sort the results - @return A list of tutorial meta-data that corresponds to the query - """ - raise NotImplementedError("get_tutorials() not implemented") - - def get_tutorial_collection(self, collection_name): - """ - Returns a list of tutorials corresponding to the given collection name. - Collections can be groups like '5 most downloaded' or 'Top 10 ratings'. - - @param collection_name The name of the collection from which we want the - meta-data - @return A list of tutorial meta-data corresponding to the given group - """ - raise NotImplementedError("get_tutorial_collection() not implemented... yet!") - - def get_latest_version(self, tutorial_id_list): - """ - Returns the latest version number on the server, for each tutorial ID - in the list. - - @param tutorial_id_list The list of tutorial IDs from which we want to - known the latest version number. - @return A dictionary having the tutorial ID as the key and the version - as the value. - """ - raise NotImplementedError("get_latest_version() not implemented") - - def download_tutorial(self, tutorial_id, version=None): - """ - Fetches the tutorial file from the server and returns the - - @param tutorial_id The tutorial that we want to get - @param version The version number that we want to download. If None, - the latest version will be downloaded. - @return The downloaded file itself (an in-memory representation of the file, - not a path to it on the disk) - - TODO : We should decide if we're saving to disk or in mem. - """ - raise NotImplementedError("downloadTutorial() not implemented") - - def login(self, username, password): - """ - Logs in the user on the store and saves the login status in the proxy - state. After a successful logon, the operation requiring a login will - be successful. - - @return True if the login was successful, False otherwise - """ - raise NotImplementedError("login() not implemented yet") - - def close_session(self): - """ - Ends the user's session on the server and changes the state of the proxy - to disallow the calls to the store that requires to be logged in. - - @return True if the user was disconnected, False otherwise - """ - raise NotImplementedError("close_session() not implemented yet") - - def get_session_id(self): - """ - Gives the current session ID cached in the Store Proxy, or returns - None is the user is not logged yet. - - @return The current session's ID, or None if the user is not logged - """ - raise NotImplementedError("get_session_id() not implemented yet") - - def rate(self, value, tutorial_store_id): - """ - Sends a rating for the given tutorial. - - This function requires the user to be logged in. - - @param value The value of the rating. It must be an integer with a value - from 1 to 5. - @param tutorial_store_id The ID of the tutorial that was rated - @return True if the rating was sent to the Store, False otherwise. - """ - raise NotImplementedError("rate() not implemented") - - def publish(self, tutorial): - """ - Sends a tutorial to the store. - - This function requires the user to be logged in. - - @param tutorial The tutorial file to be sent. Note that this is the - content itself and not the path to the file. - @return True if the tutorial was sent correctly, False otherwise. - """ - raise NotImplementedError("publish() not implemented") - - def unpublish(self, tutorial_store_id): - """ - Removes a tutorial from the server. The user in the current session - needs to be the creator for it to be unpublished. This will remove - the file from the server and from all its collections and categories. - - This function requires the user to be logged in. - - @param tutorial_store_id The ID of the tutorial to be removed - @return True if the tutorial was properly removed from the server - """ - raise NotImplementedError("unpublish() not implemented") - - def update_published_tutorial(self, tutorial_id, tutorial): - """ - Sends the new content for the tutorial with the given ID. - - This function requires the user to be logged in. - - @param tutorial_id The ID of the tutorial to be updated - @param tutorial The bundled tutorial file content (not a path!) - @return True if the tutorial was sent and updated, False otherwise - """ - raise NotImplementedError("update_published_tutorial() not implemented yet") - - def register_new_user(self, user_info): - """ - Creates a new user from the given user information. - - @param user_info A structure containing all the data required to do a login. - @return True if the new account was created, false otherwise - """ - raise NotImplementedError("register_new_user() not implemented") diff --git a/tutorius/testwin.py b/tutorius/testwin.py deleted file mode 100644 index ef92b7f..0000000 --- a/tutorius/testwin.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/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 deleted file mode 100644 index e09b298..0000000 --- a/tutorius/textbubble.py +++ /dev/null @@ -1,109 +0,0 @@ -# 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 deleted file mode 100644 index bcd67e1..0000000 --- a/tutorius/uam/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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) - ## Commenting this line as it is causing an error in the tests - ##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 deleted file mode 100644 index c1fba3d..0000000 --- a/tutorius/uam/gobjectparser.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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 deleted file mode 100644 index ede2f03..0000000 --- a/tutorius/uam/gtkparser.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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 - -<scheme>://<activity>/<path>[?<params>#<ptype>] - -where: - -<scheme> is the uam.SCHEME + "." + SCHEME - -<activity> is the activity's dns identifier, such as battleship.tutorius.org - -<path> is the Hierarchical path to the widget, where 0 is the activity, such as /0/0/1/0/1/0 - -<params> can be used to specify additionnal parameters required for an event handler or action, such as event=clicked - -<ptype> 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 diff --git a/tutorius/vault.py b/tutorius/vault.py deleted file mode 100644 index b455a52..0000000 --- a/tutorius/vault.py +++ /dev/null @@ -1,860 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Jean-Christophe Savard <savard.jean.christophe@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 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 shutil -import tempfile -import uuid -import xml.dom.minidom -from xml.dom import NotFoundErr -import zipfile -from ConfigParser import SafeConfigParser - -from . import addon -from .core import Tutorial, State, FiniteStateMachine - -logger = logging.getLogger("tutorius") - -# this is where user installed/generated tutorials will go -def _get_store_root(): - profile_name = os.getenv("SUGAR_PROFILE") or "default" - return os.path.join(os.getenv("HOME"), - ".sugar",profile_name,"tutorius","data") -# this is where activity bundled tutorials should be, under the activity bundle -def _get_bundle_root(): - """ - Return the path of the bundled activity, or None if not applicable. - """ - if os.getenv("SUGAR_BUNDLE_PATH") != None: - return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data") - else: - return None - -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_VERSION_PROPERTY = 'version' -INI_FILENAME = "meta.ini" -TUTORIAL_FILENAME = "tutorial.xml" -NODE_COMPONENT = "Component" -NODE_SUBCOMPONENT = "property" -NODE_SUBCOMPONENTLIST = "listproperty" -NEXT_STATE_ATTR = "next_state" - -class Vault(object): - - ## Vault internal functions : - @staticmethod - def list_available_tutorials(activity_name = None, activity_vers = 0): - """ - Generate the list of all tutorials present on disk for a - given activity. - - @param activity_name the name of the activity associated with this tutorial. None means ALL activities - @param activity_vers the version number of the activity to find tutorail for. 0 means find for ANY version. If activity_name is None, version number is not used - @returns a map of tutorial {names : GUID}. - """ - # check both under the activity data and user installed folders - if _get_bundle_root() != None: - paths = [_get_store_root(), _get_bundle_root()] - else: - paths = [_get_store_root()] - - tutoGuidName = {} - - for repository in paths: - # (our) convention dictates that tutorial folders are named - # with their GUID (for unicity) - try: - for tuto in os.listdir(repository): - parser = SafeConfigParser() - file = parser.read(os.path.join(repository, tuto, INI_FILENAME)) - if file != []: - # If parser can read at least section - 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 != None) and (activity_name.lower() in activities): - version = parser.get(INI_ACTIVITY_SECTION, activity_name) - if (activity_vers == version) or (activity_vers == 0): - tutoGuidName[guid] = name - elif (activity_name == None): - tutoGuidName[guid] = name - except OSError: - # the repository may not exist. Continue scanning - pass - - return tutoGuidName - - ## Vault interface functions : - @staticmethod - def installTutorials(path, zip_file_name, forceinstall=False): - """ - Extract the tutorial files in the ZIPPED tutorial archive at the - specified path and add them inside the vault. This should remove any previous - version of this tutorial, if there's any. On the opposite, if we are - trying to install an earlier version, the function will return 1 if - forceInstall is not set to true. - - @params path The path where the zipped tutorial archive is present - @params forceinstall A flag that indicate if we need to force overwrite - of a tutorial even if is version number is lower than the existing one. - - @returns 0 if it worked, 1 if the user needs to confirm the installation - and 2 to mean an error happened - """ - # TODO : Check with architecture team for exception vs error returns - - # test if the file is a valid pkzip file - if zipfile.is_zipfile(os.path.join(path, zip_file_name)) != True: - assert False, "Error : The given file is not a valid PKZip file" - - # unpack the zip archive - zfile = zipfile.ZipFile(os.path.join(path, zip_file_name), "r" ) - - temp_path = tempfile.mkdtemp(dir=_get_store_root()) - zfile.extractall(temp_path) - - # get the tutorial file - ini_file_path = os.path.join(temp_path, INI_FILENAME) - ini_file = SafeConfigParser() - ini_file.read(ini_file_path) - - # get the tutorial guid - guid = ini_file.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) - - # Check if tutorial already exist - tutorial_path = os.path.join(_get_store_root(), guid) - if os.path.isdir(tutorial_path) == False: - # Copy the tutorial in the Vault - shutil.copytree(temp_path, tutorial_path) - - else: - # Check the version of the existing tutorial - existing_version = ini_file.get(INI_METADATA_SECTION, INI_VERSION_PROPERTY) - # Check the version of the new tutorial - new_ini_file = SafeConfigParser() - new_ini_file.read(os.path.join(tutorial_path, INI_FILENAME)) - new_version = new_ini_file.get(INI_METADATA_SECTION, INI_VERSION_PROPERTY) - - if new_version < existing_version and forceinstall == False: - # Version of new tutorial is older and forceinstall is false, return exception - return 1 - else : - # New tutorial is newer or forceinstall flag is set, can overwrite the existing tutorial - shutil.rmtree(tutorial_path) - shutil.copytree(temp_path, tutorial_path) - - # Remove temp data - shutil.rmtree(temp_path) - - return 0 - - @staticmethod - def query(keyword=[], relatedActivityNames=[], category=[]): - """ - Returns the list of tutorials that corresponds to the specified parameters. - - @returns a list of Tutorial meta-data (TutorialID, Description, - Rating, Category, PublishState, etc...) - TODO : Search for tuto caracterised by the entry : OR between [], and between each - - The returned dictionnary is of this format : key = property name, value = property value - The dictionnary also contain one dictionnary element whose key is the string 'activities' - and whose value is another dictionnary of this form : key = related activity name, - value = related activity version number - """ - - # Temp solution for returning all tutorials metadata - - tutorial_list = [] - tuto_guid_list = [] - ini_file = SafeConfigParser() - if keyword == [] and relatedActivityNames == [] and category == []: - # get all tutorials tuples (name:guid) for all activities and version - tuto_dict = Vault.list_available_tutorials() - for id in tuto_dict.keys(): - tuto_guid_list.append(id) - - # Find .ini metadata files with the guid list - - # Get the guid from the tuto tuples - for guid in tuto_guid_list: - # Create a dictionnary containing the metadata and also - # another dictionnary containing the tutorial Related Acttivities, - # and add it to a list - - # Create a TutorialBundler object from the guid - bundler = TutorialBundler(guid) - # Find the .ini file path for this guid - ini_file_path = bundler.get_tutorial_path(guid) - # Read the .ini file - ini_file.read(os.path.join(ini_file_path, 'meta.ini')) - - metadata_dictionnary = {} - related_act_dictionnary = {} - metadata_list = ini_file.options(INI_METADATA_SECTION) - for metadata_name in metadata_list: - # Create a dictionnary of tutorial metadata - metadata_dictionnary[metadata_name] = ini_file.get(INI_METADATA_SECTION, metadata_name) - # Get Related Activities data from.ini files - related_act_list = ini_file.options(INI_ACTIVITY_SECTION) - for related_act in related_act_list: - # For related activites, the format is : key = activity name, value = activity version - related_act_dictionnary[related_act] = ini_file.get(INI_ACTIVITY_SECTION, related_act) - - # Add Related Activities dictionnary to metadata dictionnary - metadata_dictionnary['activities'] = related_act_dictionnary - - # Add this dictionnary to tutorial list - tutorial_list.append(metadata_dictionnary) - - # Return tutorial list - return tutorial_list - - @staticmethod - def loadTutorial(Guid): - """ - Creates an executable version of a tutorial from its saved representation. - @returns an executable representation of a tutorial - """ - - bundle = TutorialBundler(Guid) - bundle_path = bundle.get_tutorial_path(Guid) - 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, bundle_path) - - tuto = Tutorial(name, fsm) - return tuto - - @staticmethod - def saveTutorial(tutorial, metadata_dict): - """ - Creates a persistent version of a tutorial in the Vault. - @returns true if the tutorial was saved correctly - """ - - # Get the tutorial guid from metadata dictionnary - guid = metadata_dict[INI_GUID_PROPERTY] - - # Check if tutorial already exist - tutorial_path = os.path.join(_get_store_root(), guid) - if os.path.isdir(tutorial_path) == False: - - # Serialize the tutorial and write it to disk - xml_ser = XMLSerializer() - os.makedirs(tutorial_path) - xml_ser.save_fsm(tutorial.state_machine, TUTORIAL_FILENAME, tutorial_path) - - # Create the metadata file - ini_file_path = os.path.join(tutorial_path, "meta.ini") - parser = SafeConfigParser() - parser.add_section(INI_METADATA_SECTION) - for key, value in metadata_dict.items(): - if key != 'activities': - parser.set(INI_METADATA_SECTION, key, value) - else: - related_activities_dict = value - parser.add_section(INI_ACTIVITY_SECTION) - for related_key, related_value in related_activities_dict.items(): - parser.set(INI_ACTIVITY_SECTION, related_key, related_value) - - # Write the file to disk - with open(ini_file_path, 'wb') as configfile: - parser.write(configfile) - - else: - # Error, tutorial already exist - return False - - # TODO : wait for Ben input on how to unpublish tuto before coding this function - # For now, no unpublishing will occur. - - - @staticmethod - def deleteTutorial(Tutorial): - """ - Removes the tutorial from the Vault. It will unpublish the tutorial if need be, - and it will also wipe it from the persistent storage. - @returns true is the tutorial was deleted from the Vault - """ - bundle = TutorialBundler(Guid) - bundle_path = bundle.get_tutorial_path(Guid) - - # TODO : Need also to unpublish tutorial, need to interact with webservice module - - shutil.rmtree(bundle_path) - if os.path.isdir(bundle_path) == False: - return True - else: - return False - - -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. - """ - raise NotImplementedError() - - def load_fsm(self): - """ - Load fsm from disk. - """ - raise 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_addon_component_node(self, parent_attr_name, comp, doc): - """ - Takes a component that is embedded in another component (e.g. the content - of a OnceWrapper) and encapsulate it in a node with the property name. - - e.g. - <Component Class="OnceWrapper"> - <property name="addon"> - <Component Class="BubbleMessage" message="'Hi!'" position="[12,32]"/> - </property> - </Component> - - When reloading this node, we should look up the property name for the parent - in the attribute of the node, then examine the subnode to create the addon - object itself. - - @param parent_attr_name The name of the parent's attribute for this addon - e.g. the OnceWrapper has the action attribute, which corresponds to a - sub-action it must execute once. - @param comp The component node itself - @param doc The XML document root (only used to create the nodes) - @returns A NODE_SUBCOMPONENT node, with the property attribute and a sub node - that represents another component. - """ - subCompNode = doc.createElement(NODE_SUBCOMPONENT) - subCompNode.setAttribute("name", parent_attr_name) - - subNode = self._create_component_node(comp, doc) - - subCompNode.appendChild(subNode) - - return subCompNode - - def _create_addonlist_component_node(self, parent_attr_name, comp_list, doc): - """ - Takes a list of components that are embedded in another component (ex. the - content of a ChainAction) and encapsulate them in a node with the property - name. - - e.g. - <Component Class="ChainAction"> - <listproperty name="actions"> - <Component Class="BubbleMessage" message="'Hi!'" position="[15,35]"/> - <Component Class="DialogMessage" message="'Multi-action!'" position="[45,10]"/> - </listproperty> - </Component> - - When reloading this node, we should look up the property name for the parent - in the the attribute of the node, then rebuild the list by appending the - content of all the subnodes. - - @param parent_attr_name The name of the parent component's property - @param comp_list A list of components that comprise the property - @param doc The XML document root (only for creating new nodes) - @returns A NODE_SUBCOMPONENTLIST node with the property attribute - """ - subCompListNode = doc.createElement(NODE_SUBCOMPONENTLIST) - subCompListNode.setAttribute("name", parent_attr_name) - - for comp in comp_list: - compNode = self._create_component_node(comp, doc) - subCompListNode.appendChild(compNode) - - return subCompListNode - - 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": - compNode.appendChild(self._create_addonlist_component_node(propname, propval, doc)) - elif getattr(type(comp), propname).type == "addon": - #import rpdb2; rpdb2.start_embedded_debugger('pass') - compNode.appendChild(self._create_addon_component_node(propname, propval, 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 an event filters. - """ - eventFiltersList = doc.createElement("EventFiltersList") - for event, state in event_filters: - eventFilterNode = self._create_component_node(event, doc) - eventFilterNode.setAttribute(NEXT_STATE_ATTR, str(state)) - 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 _get_direct_descendants_by_tag_name(self, node, name): - """ - Searches in the list of direct descendants of a node to find all the node - that have the given name. - - This is used because the Document.getElementsByTagName() function returns the - list of all the descendants (whatever their distance to the start node) that - have that name. In the case of complex components, we absolutely need to inspect - a single layer of the tree at the time. - - @param node The node from which we want the direct descendants with a particular - name - @param name The name of the node - @returns A list, possibly empty, of direct descendants of node that have this name - """ - return_list = [] - for childNode in node.childNodes: - if childNode.nodeName == name: - return_list.append(childNode) - return return_list - - -## 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 - """ - transition_list = [] - event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) - new_event_filter = None - - for event_filter in event_filter_element_list: - next_state = event_filter.getAttribute(NEXT_STATE_ATTR) - try: - event_filter.removeAttribute(NEXT_STATE_ATTR) - except NotFoundErr: - next_state = None - new_event_filter = self._load_xml_component(event_filter) - - if new_event_filter is not None: - transition_list.append((new_event_filter, next_state)) - - return transition_list - - def _load_xml_subcomponents(self, node, properties): - """ - Loads all the subcomponent node below the given node and inserts them with - the right property name inside the properties dictionnary. - - @param node The parent node that contains one or many property nodes. - @param properties A dictionnary where the subcomponent property names - and the instantiated components will be stored - @returns Nothing. The properties dict will contain the property->comp mapping. - """ - subCompList = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT) - - for subComp in subCompList: - property_name = subComp.getAttribute("name") - internal_comp_node = self._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] - internal_comp = self._load_xml_component(internal_comp_node) - properties[str(property_name)] = internal_comp - - def _load_xml_subcomponent_lists(self, node, properties): - """ - Loads all the subcomponent lists below the given node and stores them - under the correct property name for that node. - - @param node The node from which we want to read the subComponent lists - @param properties The dictionnary that will contain the mapping of prop->subCompList - @returns Nothing. The values are returns inside the properties dict. - """ - listOf_subCompListNode = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST) - for subCompListNode in listOf_subCompListNode: - property_name = subCompListNode.getAttribute("name") - subCompList = [] - for subCompNode in self._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): - subComp = self._load_xml_component(subCompNode) - subCompList.append(subComp) - properties[str(property_name)] = subCompList - - 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 - """ - class_name = node.getAttribute("Class") - - properties = {} - - for prop in node.attributes.keys(): - if prop == "Class" : continue - # security : keep sandboxed - properties[str(prop)] = eval(node.getAttribute(prop)) - - # Read the complex attributes - self._load_xml_subcomponents(node, properties) - self._load_xml_subcomponent_lists(node, properties) - - new_action = addon.create(class_name, **properties) - - if not new_action: - return None - - 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 = self._get_direct_descendants_by_tag_name(actions_elem, 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 <States> 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, next_state in events: - fsm.add_event_filter(event, next_state) - - return fsm - - - def load_fsm(self, guid, path=None): - """ - Load fsm from xml file whose .ini file guid match argument guid. - """ - # Fetch the directory (if any) - bundler = TutorialBundler(guid) - tutorial_dir = bundler.get_tutorial_path(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, bundle_path=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()) - - #FIXME: Look for the bundle in the activity first (more specific) - #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(), str(generated_guid), INI_FILENAME) - if os.path.isfile(store_path): - self.Path = os.path.dirname(store_path) - elif _get_bundle_root() != None: - #Bundle store - bundle_path = os.path.join(_get_bundle_root(), str(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: - 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) - if os.environ['SUGAR_BUNDLE_NAME'] != None and os.environ['SUGAR_BUNDLE_VERSION'] != None: - cfg.set(INI_ACTIVITY_SECTION, os.environ['SUGAR_BUNDLE_NAME'], - os.environ['SUGAR_BUNDLE_VERSION']) - else: - cfg.set(INI_ACTIVITY_SECTION, 'not_an_activity', '0') - - #Write the ini file - cfg.write( file( os.path.join(self.Path, INI_FILENAME), 'w' ) ) - - - @staticmethod - def get_tutorial_path(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(),str(guid)) - # Then in the activty's bundle path - if _get_bundle_root() != None: - activity_dir = os.path.join(_get_bundle_root(), str(guid)) - else: - activity_dir = '' - - # 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 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) - - @staticmethod - def add_resources(typename, file): - """ - Add ressources to metadata. - """ - raise NotImplementedError("add_resources not implemented") diff --git a/tutorius/viewer.py b/tutorius/viewer.py deleted file mode 100644 index 272558e..0000000 --- a/tutorius/viewer.py +++ /dev/null @@ -1,423 +0,0 @@ -# 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 renders a widget containing a graphical representation -of a tutorial and acts as a creator proxy as it has some editing -functionality. -""" -import sys - -import gtk, gtk.gdk -import cairo -from math import pi as PI -PI2 = PI/2 - -import rsvg - -from sugar.bundle import activitybundle -from sugar.tutorius import addon -from sugar.graphics import icon -from sugar.tutorius.filters import EventFilter -from sugar.tutorius.actions import Action -import os - -# FIXME ideally, apps scale correctly and we should use proportional positions -X_WIDTH = 800 -X_HEIGHT = 600 -ACTION_WIDTH = 100 -ACTION_HEIGHT = 70 - -# block look -BLOCK_PADDING = 5 -BLOCK_WIDTH = 100 -BLOCK_CORNERS = 10 -BLOCK_INNER_PAD = 10 - -SNAP_WIDTH = BLOCK_WIDTH - BLOCK_PADDING - BLOCK_INNER_PAD*2 -SNAP_HEIGHT = SNAP_WIDTH*X_HEIGHT/X_WIDTH -SNAP_SCALE = float(SNAP_WIDTH)/X_WIDTH - -class Viewer(object): - """ - Renders a tutorial as a sequence of blocks, each block representing either - an action or an event (transition). - - Current Viewer implementation lacks viewport management; - having many objects in a tutorial will not render properly. - """ - def __init__(self, tutorial, creator): - super(Viewer, self).__init__() - - self._tutorial = tutorial - self._creator = creator - self.alloc = None - self.click_pos = None - self.drag_pos = None - self.selection = [] - - self.win = gtk.Window(gtk.WINDOW_TOPLEVEL) - self.win.set_size_request(400, 200) - self.win.set_gravity(gtk.gdk.GRAVITY_SOUTH_WEST) - self.win.show() - self.win.set_deletable(False) - self.win.move(0, 0) - - vbox = gtk.ScrolledWindow() - self.win.add(vbox) - - canvas = gtk.DrawingArea() - vbox.add_with_viewport(canvas) - canvas.set_app_paintable(True) - canvas.connect_after("expose-event", self.on_viewer_expose, tutorial._states) - canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK \ - |gtk.gdk.BUTTON_MOTION_MASK \ - |gtk.gdk.BUTTON_RELEASE_MASK \ - |gtk.gdk.KEY_PRESS_MASK) - canvas.connect('button-press-event', self._on_click) - # drag-select disabled, for now - #canvas.connect('motion-notify-event', self._on_drag) - canvas.connect('button-release-event', self._on_drag_end) - canvas.connect('key-press-event', self._on_key_press) - - canvas.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS) - canvas.grab_focus() - - self.win.show_all() - canvas.set_size_request(2048, 180) # FIXME - - def destroy(self): - self.win.destroy() - - - def _paint_state(self, ctx, states): - """ - Paints a tutorius fsm state in a cairo context. - Final context state will be shifted by the size of the graphics. - """ - block_width = BLOCK_WIDTH - BLOCK_PADDING - block_max_height = self.alloc.height - - new_insert_point = None - cur_state = 'INIT' - - # FIXME: get app when we have a model that supports it - cur_app = 'Calculate' - app_start = ctx.get_matrix() - try: - state = states[cur_state] - except KeyError: - state = None - - while state: - new_app = 'Calculate' - if new_app != cur_app: - ctx.save() - ctx.set_matrix(app_start) - self._render_app_hints(ctx, cur_app) - ctx.restore() - app_start = ctx.get_matrix() - ctx.translate(BLOCK_PADDING, 0) - cur_app = new_app - - action_list = state.get_action_list() - if action_list: - local_height = (block_max_height - BLOCK_PADDING)/len(action_list) - BLOCK_PADDING - ctx.save() - for action in action_list: - origin = tuple(ctx.get_matrix())[-2:] - if self.click_pos and \ - self.click_pos[0]-BLOCK_WIDTH<origin[0] and \ - self.drag_pos[0]>origin[0]: - self.selection.append(action) - self.render_action(ctx, block_width, local_height, action) - ctx.translate(0, local_height+BLOCK_PADDING) - - ctx.restore() - ctx.translate(BLOCK_WIDTH, 0) - - # insertion cursor painting made from two opposed triangles - # joined by a line. - if state.name == self._creator.get_insertion_point(): - ctx.save() - bp2 = BLOCK_PADDING/2 - ctx.move_to(-bp2, 0) - ctx.line_to(-BLOCK_PADDING-bp2, -BLOCK_PADDING) - ctx.line_to(bp2, -BLOCK_PADDING) - ctx.line_to(-bp2, 0) - - ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING) - ctx.line_to(bp2, block_max_height-BLOCK_PADDING) - ctx.line_to(-BLOCK_PADDING-bp2, block_max_height-BLOCK_PADDING) - ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING) - - ctx.line_to(-bp2, BLOCK_PADDING) - ctx.set_source_rgb(1.0, 1.0, 0.0) - ctx.stroke_preserve() - ctx.fill() - ctx.restore() - - - event_list = state.get_event_filter_list() - if event_list: - local_height = (block_max_height - BLOCK_PADDING)/len(event_list) - BLOCK_PADDING - ctx.save() - for event, next_state in event_list: - origin = tuple(ctx.get_matrix())[-2:] - if self.click_pos and \ - self.click_pos[0]-BLOCK_WIDTH<origin[0] and \ - self.drag_pos[0]>origin[0]: - self.selection.append(event) - self.render_event(ctx, block_width, local_height, event) - ctx.translate(0, local_height+BLOCK_PADDING) - - ctx.restore() - ctx.translate(BLOCK_WIDTH, 0) - - # FIXME point to next state in state, as it would highlight - # the "happy path". - cur_state = event_list[0][1] - - if (not new_insert_point) and self.click_pos: - origin = tuple(ctx.get_matrix())[-2:] - if self.click_pos[0]<origin[0]: - new_insert_point = state - - if event_list: - try: - state = states[cur_state] - except KeyError: - break - yield True - else: - break - - ctx.set_matrix(app_start) - self._render_app_hints(ctx, cur_app) - - if self.click_pos: - if not new_insert_point: - new_insert_point = state - - self._creator.set_insertion_point(new_insert_point.name) - - yield False - - def _render_snapshot(self, ctx, elem): - """ - Render the "simplified screenshot-like" representation of elements positions. - """ - ctx.set_source_rgba(1.0, 1.0, 1.0, 0.5) - ctx.rectangle(0, 0, SNAP_WIDTH, SNAP_HEIGHT) - ctx.fill_preserve() - ctx.stroke() - - if hasattr(elem, 'position'): - pos = elem.position - # FIXME this size approximation is fine, but I believe we could - # do better. - ctx.scale(SNAP_SCALE, SNAP_SCALE) - ctx.rectangle(pos[0], pos[1], ACTION_WIDTH, ACTION_HEIGHT) - ctx.fill_preserve() - ctx.stroke() - - def _render_app_hints(self, ctx, appname): - """ - Fetches the icon of the app related to current states and renders it on a - separator, between states. - """ - ctx.set_source_rgb(0.0, 0.0, 0.0) - ctx.set_dash((1,1,0,0), 1) - ctx.move_to(0, 0) - ctx.line_to(0, self.alloc.height) - ctx.stroke() - ctx.set_dash(tuple(), 1) - - bundle_path = os.getenv("SUGAR_BUNDLE_PATH") - if bundle_path: - icon_path = activitybundle.ActivityBundle(bundle_path).get_icon() - icon = rsvg.Handle(icon_path) - ctx.save() - ctx.translate(-15, 0) - ctx.scale(0.5, 0.5) - icon_surf = icon.render_cairo(ctx) - ctx.restore() - - - def render_action(self, ctx, width, height, action): - """ - Renders the action block, along with the icon of the action tool. - """ - ctx.save() - inner_width = width-(BLOCK_CORNERS<<1) - inner_height = height-(BLOCK_CORNERS<<1) - - paint_border = ctx.rel_line_to - filling = cairo.LinearGradient(0, 0, 0, inner_height) - if action not in self.selection: - filling.add_color_stop_rgb(0.0, 0.7, 0.7, 1.0) - filling.add_color_stop_rgb(1.0, 0.1, 0.1, 0.8) - else: - filling.add_color_stop_rgb(0.0, 0.4, 0.4, 0.8) - filling.add_color_stop_rgb(1.0, 0.0, 0.0, 0.5) - tracing = cairo.LinearGradient(0, 0, 0, inner_height) - tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0) - tracing.add_color_stop_rgb(1.0, 0.2, 0.2, 0.2) - - ctx.move_to(BLOCK_CORNERS, 0) - paint_border(inner_width, 0) - ctx.arc(inner_width+BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI2, 0.0) - ctx.arc(inner_width+BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, 0.0, PI2) - ctx.arc(BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, PI2, PI) - ctx.arc(BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI, -PI2) - - ctx.set_source(tracing) - ctx.stroke_preserve() - ctx.set_source(filling) - ctx.fill() - - addon_name = addon.get_name_from_type(type(action)) - # TODO use icon pool - icon_name = addon.get_addon_meta(addon_name)['icon'] - rsvg_icon = rsvg.Handle(icon.get_icon_file_name(icon_name)) - ctx.save() - ctx.translate(BLOCK_INNER_PAD, BLOCK_INNER_PAD) - ctx.scale(0.5, 0.5) - icon_surf = rsvg_icon.render_cairo(ctx) - - ctx.restore() - - ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2) - self._render_snapshot(ctx, action) - - ctx.restore() - - def render_event(self, ctx, width, height, event): - """ - Renders the action block, along with the icon of the action tool. - """ - ctx.save() - inner_width = width-(BLOCK_CORNERS<<1) - inner_height = height-(BLOCK_CORNERS<<1) - - filling = cairo.LinearGradient(0, 0, 0, inner_height) - if event not in self.selection: - filling.add_color_stop_rgb(0.0, 1.0, 0.8, 0.6) - filling.add_color_stop_rgb(1.0, 1.0, 0.6, 0.2) - else: - filling.add_color_stop_rgb(0.0, 0.8, 0.6, 0.4) - filling.add_color_stop_rgb(1.0, 0.6, 0.4, 0.1) - tracing = cairo.LinearGradient(0, 0, 0, inner_height) - tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0) - tracing.add_color_stop_rgb(1.0, 0.3, 0.3, 0.3) - - ctx.move_to(BLOCK_CORNERS, 0) - ctx.rel_line_to(inner_width, 0) - ctx.rel_line_to(BLOCK_CORNERS, BLOCK_CORNERS) - ctx.rel_line_to(0, inner_height) - ctx.rel_line_to(-BLOCK_CORNERS, BLOCK_CORNERS) - ctx.rel_line_to(-inner_width, 0) - ctx.rel_line_to(-BLOCK_CORNERS, -BLOCK_CORNERS) - ctx.rel_line_to(0, -inner_height) - ctx.close_path() - - ctx.set_source(tracing) - ctx.stroke_preserve() - ctx.set_source(filling) - ctx.fill() - - addon_name = addon.get_name_from_type(type(event)) - # TODO use icon pool - icon_name = addon.get_addon_meta(addon_name)['icon'] - rsvg_icon = rsvg.Handle(icon.get_icon_file_name(icon_name)) - ctx.save() - ctx.translate(BLOCK_INNER_PAD, BLOCK_INNER_PAD) - ctx.scale(0.5, 0.5) - icon_surf = rsvg_icon.render_cairo(ctx) - - ctx.restore() - - ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2) - self._render_snapshot(ctx, event) - - ctx.restore() - - def on_viewer_expose(self, widget, evt, states): - """ - Expose signal handler for the viewer's DrawingArea. - This loops through states and renders every action and transition of - the "happy path". - - @param widget: the gtk.DrawingArea on which to draw - @param evt: the gtk.gdk.Event containing an "expose" event - @param states: a tutorius FiniteStateMachine object to paint - """ - ctx = widget.window.cairo_create() - self.alloc = widget.get_allocation() - ctx.set_source_pixmap(widget.window, - widget.allocation.x, - widget.allocation.y) - - # draw no more than our expose event intersects our child - region = gtk.gdk.region_rectangle(widget.allocation) - r = gtk.gdk.region_rectangle(evt.area) - region.intersect(r) - ctx.region (region) - ctx.clip() - ctx.paint() - - ctx.translate(BLOCK_PADDING, BLOCK_PADDING) - - painter = self._paint_state(ctx, states) - while painter.next(): pass - - if self.click_pos and self.drag_pos: - ctx.set_matrix(cairo.Matrix()) - ctx.rectangle(self.click_pos[0], self.click_pos[1], - self.drag_pos[0]-self.click_pos[0], - self.drag_pos[1]-self.click_pos[1]) - ctx.set_source_rgba(0, 0, 1, 0.5) - ctx.fill_preserve() - ctx.stroke() - - return False - - def _on_click(self, widget, evt): - # the rendering pipeline will work out the click validation process - self.drag_pos = None - self.drag_pos = self.click_pos = evt.get_coords() - widget.queue_draw() - - self.selection = [] - - def _on_drag(self, widget, evt): - self.drag_pos = evt.get_coords() - widget.queue_draw() - - def _on_drag_end(self, widget, evt): - self.click_pos = self.drag_pos = None - widget.queue_draw() - - def _on_key_press(self, widget, evt): - if evt.keyval == gtk.keysyms.BackSpace: - # remove selection - for selected in self.selection: - if isinstance(selected, EventFilter): - self._creator.delete_state() - else: - self._creator.delete_action(selected) - widget.queue_draw() - - |