From 88b3f3099252353e22c536a68e15e2a2f9b10334 Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 19 Nov 2009 14:38:39 +0000 Subject: LP 448319 : Addition of resource properties, insertion of BubbleMessageWImg from Dave, modification of Engine to get action_addresses --- (limited to 'tutorius') diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index 0c79690..b4b1826 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -297,14 +297,15 @@ class ProbeProxy: except: return False - def __update_action(self, action, address): + def __update_action(self, action, callback, address): LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address)) self._actions[action] = str(address) + callback(address) def __clear_action(self, action): self._actions.pop(action, None) - def install(self, action, block=False): + def install(self, action, callback, block=False): """ Install an action on the TProbe's activity @param action Action to install @@ -312,10 +313,10 @@ class ProbeProxy: @return None """ return remote_call(self._probe.install, (pickle.dumps(action),), - save_args(self.__update_action, action), + save_args(self.__update_action, action, callback), block=block) - def update(self, action, newaction, block=False): + def update(self, action_address, newaction, block=False): """ Update an already installed action's properties and run it again @param action Action to update @@ -324,19 +325,20 @@ class ProbeProxy: @return None """ #TODO review how to make this work well - if not action in self._actions: + if not action_address in self._actions.values(): raise RuntimeWarning("Action not installed") #TODO Check error handling - return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block) + return remote_call(self._probe.update, (action_address, pickle.dumps(newaction._props)), block=block) - def uninstall(self, action, block=False): + def uninstall(self, action_address, 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) + for (action, action_address) in self._actions.items(): + remote_call(self._probe.uninstall,(action_address,), block=block) + del self._actions[action] def __update_event(self, event, callback, address): LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) @@ -465,7 +467,7 @@ class ProbeManager(object): currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) - def install(self, action, block=False): + def install(self, action, callback, block=False): """ Install an action on the current activity @param action Action to install @@ -473,31 +475,31 @@ class ProbeManager(object): @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).install(action, block) + return self._first_proxy(self.currentActivity).install(action, callback, block) else: raise RuntimeWarning("No activity attached") - def update(self, action, newaction, block=False): + def update(self, action_address, newaction, block=False): """ Update an already installed action's properties and run it again - @param action Action to update + @param action_address 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._first_proxy(self.currentActivity).update(action, newaction, block) + return self._first_proxy(self.currentActivity).update(action_address, newaction, block) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action, block=False): + def uninstall(self, action_address, 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._first_proxy(self.currentActivity).uninstall(action, block) + return self._first_proxy(self.currentActivity).uninstall(action_address, block) else: raise RuntimeWarning("No activity attached") diff --git a/tutorius/constraints.py b/tutorius/constraints.py index 519bce8..cd71167 100644 --- a/tutorius/constraints.py +++ b/tutorius/constraints.py @@ -24,6 +24,8 @@ for some properties. # For the File Constraint import os +# For the Resource Constraint +import re class ConstraintException(Exception): """ @@ -214,3 +216,40 @@ class FileConstraint(Constraint): raise FileConstraintError("Non-existing file : %s"%value) return +class ResourceConstraintError(ConstraintException): + pass + +class ResourceConstraint(Constraint): + """ + Ensures that the value is looking like a resource name, like + _[.]. We are not validating that this is a + valid resource for the reason that the property does not have any notion + of tutorial guid. + + TODO : Find a way to properly validate resources by looking them up in the + Vault. + """ + + # Regular expression to parse a resource-like name + resource_regexp_text = "(.+)_([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})(\..*)?$" + resource_regexp = re.compile(resource_regexp_text) + + def validate(self, value): + # TODO : Validate that we will not use an empty resource or if we can + # have transitory resource names + if value is None: + raise ResourceConstraintError("Resource not allowed to have a null value!") + + # Special case : We allow the empty resource name for now + if value == "": + return value + + # Attempt to see if the value has a resource name inside it + match = self.resource_regexp.search(value) + + # If there was no match on the reg exp + if not match: + raise ResourceConstraintError("Resource name does not seem to be valid : %s" % value) + + # If the name matched, then the value is _PROBABLY_ good + return value diff --git a/tutorius/engine.py b/tutorius/engine.py index c945e49..10c8d14 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -7,7 +7,9 @@ from .vault import Vault from .TProbe import ProbeManager from .dbustools import save_args from .tutorial import Tutorial, AutomaticTransitionEvent +from .translator import ResourceTranslator +LOGGER = logging.getLogger("sugar.tutorius.engine") class TutorialRunner(object): """ @@ -27,6 +29,7 @@ class TutorialRunner(object): #Cached objects self._actions = {} + self._installed_actions = {} #Temp FIX until event/actions have an activity id self._activity_id = None @@ -55,15 +58,21 @@ class TutorialRunner(object): return #Clear the current actions - for action in self._actions.values(): - self._pM.uninstall(action) + for (action_name, action_address) in self._installed_actions.items(): + LOGGER.debug("TutorialRunner :: Uninstalling action %s with address %s"%(action_name, action_address)) + self._pM.uninstall(action_address) self._actions = {} + self._installed_actions = {} #Clear the EventFilters for event in self._sEvents: self._pM.unsubscribe(event) self._sEvents.clear() + def __action_installed(self, action_name, address): + LOGGER.debug("TutorialRunner :: Action %s received address %s"%(action_name, address)) + self._installed_actions[action_name] = address + def _setupState(self): if self._state is None: raise RuntimeError("Attempting to setupState without a state") @@ -77,13 +86,15 @@ class TutorialRunner(object): for (event, next_state) in transitions.values(): if isinstance(event, AutomaticTransitionEvent): state_name = next_state - break + return state_name + + for (action_name, action) in self._actions.items(): + self._pM.install(action, save_args(self.__action_installed, action_name), True) + LOGGER.debug("TutorialRunner :: Installed action %s"%(action_name)) + for (event, next_state) in transitions.values(): self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state))) - for action in self._actions.values(): - self._pM.install(action) - return state_name def enterState(self, state_name): @@ -112,9 +123,6 @@ class TutorialRunner(object): # transition in the state definition self.enterState(self._setupState()) - - - class Engine: """ Driver for the execution of tutorials @@ -137,7 +145,8 @@ class Engine: if self._tutorial: self.stop() - self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), self._probeManager) + translator_decorator = ResourceTranslator(self._probeManager, tutorialID) + self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), translator_decorator) #Get the active activity from the shell activity = self._shell.get_active_activity() diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py index b967739..216d71a 100644 --- a/tutorius/overlayer.py +++ b/tutorius/overlayer.py @@ -319,6 +319,198 @@ class TextBubble(gtk.Widget): gobject.type_register(TextBubble) +class TextBubbleWImg(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), imagepath=None): + """ + 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 + + # image painting + self.filename = imagepath + self.pixbuf = gtk.gdk.pixbuf_new_from_file(self.filename) + self.imgsize = (self.pixbuf.get_width(), self.pixbuf.get_height()) + + 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() + + # draw/write text + 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.line_width+self.imgsize[1]+self.padding/2)) + pangoctx.show_layout(self._text_layout) + + # create a new cairo surface to place the image on + #surface = cairo.ImageSurface(0,x,y) + # create a context to the new surface + #ct = cairo.Context(surface) + + # paint image + context.set_source_pixbuf( + self.pixbuf, + int((self.allocation.width-self.imgsize[0])/2), + int(self.line_width+self.padding/2)) + + context.paint() + + # work done. Be kind to next cairo widgets and reset matrix. + context.identity_matrix() + + def do_realize(self): + """ Setup gdk window creation. """ + self.set_flags(gtk.REALIZED | gtk.NO_WINDOW) + self.window = self.get_parent_window() + if not self._no_expose: + self.__exposer = self.connect_after("expose-event", \ + self.__on_expose) + + def __on_expose(self, widget, event): + """Redraw event callback.""" + ctx = self.window.cairo_create() + + self.draw_with_context(ctx) + + return True + + def _set_label(self, value): + """Sets the label and flags the widget to be redrawn.""" + self.__label = "%s"%value + if not self.parent: + return + ctx = self.parent.window.cairo_create() + pangoctx = pangocairo.CairoContext(ctx) + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(value) + del pangoctx, ctx#, surf + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the widget.""" + ctx = self.parent.window.cairo_create() + pangoctx = pangocairo.CairoContext(ctx) + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(self.__label) + + width, height = self._text_layout.get_pixel_size() + + max_width = width + if self.imgsize[0] > width: + max_width = self.imgsize[0] + requisition.width = int(2*self.padding+max_width) + + requisition.height = int(2*self.padding+height+self.imgsize[1]) + + 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(TextBubbleWImg) + + class Rectangle(gtk.Widget): """ A CanvasDrawableWidget drawing a rectangle over a specified widget. diff --git a/tutorius/properties.py b/tutorius/properties.py index 5422532..6bd16ee 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -24,7 +24,9 @@ from copy import copy, deepcopy from .constraints import Constraint, \ UpperLimitConstraint, LowerLimitConstraint, \ MaxSizeConstraint, MinSizeConstraint, \ - ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint + ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint, \ + ResourceConstraint + class TPropContainer(object): """ @@ -89,6 +91,21 @@ class TPropContainer(object): except AttributeError: return object.__setattr__(self, name, value) + def replace_property(self, prop_name, new_prop): + """ + Changes the content of a property. This is done in order to support + the insertion of executable properties in the place of a portable + property. The typical exemple is that a resource property needs to + be changed into a file property with the correct file name, since the + installation location will be different on every platform. + + @param prop_name The name of the property to be changed + @param new_prop The new property to insert + @raise AttributeError of the mentionned property doesn't exist + """ + props = object.__getattribute__(self, "_props") + props.__setitem__(prop_name, new_prop) + def get_properties(self): """ Return the list of property names. @@ -272,6 +289,37 @@ class TFileProperty(TutoriusProperty): self.default = self.validate(path) +class TResourceProperty(TutoriusProperty): + """ + Represents a resource in the tutorial. A resource is a file with a specific + name that exists under the tutorials folder. It is distributed alongside the + tutorial itself. + + When the system encounters a resource, it knows that it refers to a file in + the resource folder and that it should translate this resource name to an + absolute file name before it is executed. + + E.g. An image is added to a tutorial in an action. On doing so, the creator + adds a resource to the tutorial, then saves its name in the resource + property of that action. When this tutorial is executed, the Engine + replaces all the TResourceProperties inside the action by their equivalent + TFileProperties with absolute paths, so that they can be used on any + machine. + """ + def __init__(self, resource_name=""): + """ + Creates a new resource pointing to an existing resource. + + @param resource_name The file name of the resource (should be only the + file name itself, no directory information) + """ + TutoriusProperty.__init__(self) + self.type = "resource" + + self.resource_cons = ResourceConstraint() + + self.default = self.validate("") + class TEnumProperty(TutoriusProperty): """ Represents a value in a given enumeration. This means that the value will diff --git a/tutorius/translator.py b/tutorius/translator.py new file mode 100644 index 0000000..d0504be --- /dev/null +++ b/tutorius/translator.py @@ -0,0 +1,204 @@ +# 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 os +import logging +import copy as copy_module + +logger = logging.getLogger("ResourceTranslator") + +from .properties import * +from .vault import Vault + +class ResourceTranslator(object): + """ + Handles the conversion of resource properties into file + properties before action execution. This class works as a decorator + to the ProbeManager class, as it is meant to be a transparent layer + before sending the action to execution. + + An architectural note : every different type of translation should have its + own method (translate_resource, etc...), and this function must be called + from the translate method, under the type test. The translate_* method + must take in the input property and give the output property that should + replace it. + """ + + def __init__(self, probe_manager, tutorial_id): + """ + Creates a new ResourceTranslator for the given tutorial. This + translator is tasked with replacing resource properties of the + incoming action into actually usable file properties pointing + to the correct resource file. This is done by querying the vault + for all the resources and creating a new file property from the + returned path. + + @param probe_manager The probe manager to decorate + @param tutorial_id The ID of the current tutorial + """ + self._probe_manager = probe_manager + self._tutorial_id = tutorial_id + + self._translation_mapping = {} + + def translate_resource(self, res_value): + """ + Replace the TResourceProperty in the container by their + runtime-defined file equivalent. Since the resources are stored + in a relative manner in the vault and that only the Vault knows + which is the current folder for the current tutorial, it needs + to transform the resource identifier into the absolute path for + the process to be able to use it properly. + + @param res_prop The resource property's value to be translated + @return The TFileProperty corresponding to this resource, containing + an absolute path to the resource + """ + # We need to replace the resource by a file representation + filepath = Vault.get_resource_path(self._tutorial_id, res_value) + logger.debug("ResourceTranslator :: Matching resource %s to file %s" % (res_value, filepath)) + + # Create the new file representation + file_prop = TFileProperty(filepath) + + return file_prop + + def translate(self, prop_container): + """ + Applies the required translation to be able to send the container to an + executing endpoint (like a Probe). For each type of property that + requires it, there is translation function that will take care of + mapping the property to its executable form. + + This function does not return anything, but its post-condition is + that all the properties of the input container have been replaced + by their corresponding executable versions. + + An example of translation is taking a resource (a relative path to + a file under the tutorial folder) and transforming it into a file + (a full absolute path) to be able to load it when the activity receives + the action. + + @param prop_container The property container in which we want to + replace all the resources for file properties and + to recursively do so for addon and addonlist + properties. + """ + for propname in prop_container.get_properties(): + prop_value = getattr(prop_container, propname) + prop_type = getattr(type(prop_container), propname).type + + # If the property is a resource, then we need to query the + # vault to create its correspondent + if prop_type == "resource": + # Apply the translation + file_prop = self.translate_resource(prop_value) + # Set the property with the new value + prop_container.replace_property(propname, file_prop) + + # If the property is an addon, then its value IS a + # container too - we need to translate it + elif prop_type == "addon": + # Translate the sub properties + self.translate(prop_value) + + # If the property is an addon list, then we need to translate all + # the elements of the list + elif prop_type == "addonlist": + # Now, for each sub-container in the list, we will apply the + # translation processing. This is done by popping the head of + # the list, translating it and inserting it back at the end. + for index in range(0, len(prop_value)): + # Pop the head of the list + container = prop_value[0] + del prop_value[0] + # Translate the sub-container + self.translate(container) + + # Put the value back in the list + prop_value.append(container) + # Change the list contained in the addonlist property, since + # we got a copy of the list when requesting it + prop_container.replace_property(propname, prop_value) + + ### ProbeManager interface for decorator ### + + ## Unchanged functions ## + def setCurrentActivity(self, activity_id): + self._probe_manager.currentActivity = activity_id + + def getCurrentActivity(self): + return self._probe_manager.currentActivity + + currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) + def attach(self, activity_id): + self._probe_manager.attach(activity_id) + + def detach(self, activity_id): + self._probe_manager.detach(activity_id) + + def subscribe(self, event, callback): + return self._probe_manager.subscribe(event, callback) + + def unsubscribe(self, address): + return self._probe_manager.unsubscribe(address) + + def register_probe(self, process_name, unique_id): + self._probe_manager.register_probe(process_name, unique_id) + + def unregister_probe(self, unique_id): + self._probe_manager.unregister_probe(unique_id) + + def get_registered_probes_list(self, process_name=None): + return self._probe_manager.get_registered_probes_list(process_name) + + ## Decorated functions ## + def install(self, action, callback, block=False): + # Make a new copy of the action that we want to install, + # because translate() changes the action and we + # don't want to modify the caller's action representation + new_action = copy_module.deepcopy(action) + # Execute the replacement + self.translate(new_action) + + # Send the new action to the probe manager + action_address = self._probe_manager.install(new_action, callback, block) + + # Remember the address + self._translation_mapping[action_address] = new_action + + return action_address + + def update(self, action_address, newaction, block=False): + # TODO : Repair this as it currently doesn't work. + # Actions are being copied, then translated in install(), so they + # won't be addressable via the same object that is in the Tutorial + # Runner. + translated_new_action = copy_module.deepcopy(newaction) + self.translate(translated_new_action) + + self._translation_mapping[action_address] = translated_new_action + + return self._probe_manager.update(action_address, translated_new_action, block) + + def uninstall(self, action_address, block=False): + return_value = self._probe_manager.uninstall(action_address, block) + + if self._translation_mapping.has_key(action_address): + del self._translation_mapping[action_address] + + return return_value + diff --git a/tutorius/vault.py b/tutorius/vault.py index fbe9e75..dc8c434 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -489,6 +489,7 @@ class Vault(object): tutorial_path = bundler.get_tutorial_path(tutorial_guid) # Check if the resource file exists file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) + logger.debug("Vault :: Assigning resource %s to file %s "%(resource_id, file_path)) if os.path.isfile(file_path): return file_path else: -- cgit v0.9.1