diff options
Diffstat (limited to 'tutorius')
-rw-r--r-- | tutorius/TProbe.py | 245 | ||||
-rw-r--r-- | tutorius/addon.py | 13 | ||||
-rw-r--r-- | tutorius/creator.py | 62 | ||||
-rw-r--r-- | tutorius/engine.py | 17 | ||||
-rw-r--r-- | tutorius/localization.py | 68 | ||||
-rw-r--r-- | tutorius/properties.py | 27 | ||||
-rw-r--r-- | tutorius/propwidgets.py | 186 | ||||
-rw-r--r-- | tutorius/service.py | 8 | ||||
-rw-r--r-- | tutorius/translator.py | 96 | ||||
-rw-r--r-- | tutorius/vault.py | 90 |
10 files changed, 742 insertions, 70 deletions
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index 5508d49..be0270a 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -14,6 +14,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import time import logging LOGGER = logging.getLogger("sugar.tutorius.TProbe") import os @@ -26,7 +27,10 @@ import cPickle as pickle from functools import partial +import gtk from jarabe.model.shell import get_model +from jarabe.model import bundleregistry +from sugar.activity import activityfactory from sugar.bundle.activitybundle import ActivityBundle from . import addon @@ -156,11 +160,13 @@ class TProbe(dbus.service.Object): if not is_editing: action.do(activity=self._activity, probe=self, overlayer=self._overlayer) else: + action.set_notification_cb(partial(self.update_action, address)) + # force mandatory props addon_name = addon.get_name_from_type(type(action)) meta = addon.get_addon_meta(addon_name) for propname in meta['mandatory_props']: - if getattr(action, propname) != None: + if getattr(action, propname, False): continue prop = getattr(type(action), propname) prop.widget_class.run_dialog(self._activity, @@ -169,7 +175,6 @@ class TProbe(dbus.service.Object): updated_props[propname] = getattr(action, propname) action.enter_editmode(overlayer=self._overlayer) - action.set_notification_cb(partial(self.update_action, address)) pickled_value = pickle.dumps((address, updated_props)) return pickled_value @@ -498,6 +503,107 @@ class DesktopProbe(TProbe): return "desktop://"+window+"/"+(".".join(name)) + # ------------------ Helper functions specific to a component -------------- + def find_widget(self, base, path, ignore_errors=True): + """ + Finds a widget from a base object. Symmetric with retrieve_path + + @param base the parent widget + @param path fqdn-style target object name + + @return widget found + """ + return find_widget(base, path, ignore_errors) + + def retrieve_path(self, widget): + """ + Retrieve the path to access a specific widget. + Symmetric with find_widget. + + @param widget the widget to find a path for + + @return path to the widget + """ + return raddr_lookup(widget) + +class FrameProbe(TProbe): + """ + Identical to the base probe except that helper functions are redefined + to handle the four windows that are part of the Frame. + """ + # ------------------ Helper functions specific to a component -------------- + def find_widget(self, base, path, ignore_errors=True): + """ + Finds a widget from a base object. Symmetric with retrieve_path + + format for the path for the frame should be: + + frame://<panel>/<path> + where panel: top | bottom | left | right + path: number[.number]* + + @param base the parent widget + @param path fqdn-style target object name + + @return widget found + """ + protocol, p = path.split("://") + assert protocol == "frame" + + window, object_id = p.split("/") + if window == "top": + return find_widget(base._top_panel, object_id, ignore_errors) + elif window == "bottom": + return find_widget(base._bottom_panel, object_id, ignore_errors) + elif window == "left": + return find_widget(base._left_panel, object_id, ignore_errors) + elif window == "right": + return find_widget(base._right_panel, object_id, ignore_errors) + else: + raise RuntimeWarning("Invalid frame panel: '%s'"%window) + + return find_widget(base, path, ignore_errors) + + def retrieve_path(self, widget): + """ + Retrieve the path to access a specific widget. + Symmetric with find_widget. + + format for the path for the frame should be: + + frame://<panel>/<path> + where panel: top | bottom | left | right + path: number[.number]* + + @param widget the widget to find a path for + + @return path to the 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() + + window = "" + if parent._position == gtk.POS_TOP: + window = "top" + elif parent._position == gtk.POS_BOTTOM: + window = "bottom" + elif parent._position == gtk.POS_LEFT: + window = "left" + elif parent._position == gtk.POS_RIGHT: + window = "right" + else: + raise RuntimeWarning("Invalid root panel in frame: %s"%str(parent)) + + return "frame://"+window+"/"+(".".join(name)) + class ProbeProxy: """ ProbeProxy is a Proxy class for connecting to a remote TProbe. @@ -673,15 +779,24 @@ class ProbeProxy: else: LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address) - def create_event(self, addon_name): + def create_event(self, addon_name, event_created_cb): """ Create an event on the app side and request the user to fill the properties before returning it. @param addon_name: the add-on name of the event + @param event_created_cb The notification to trigger once the event has + been instantiated @returns: an eventfilter instance """ - return pickle.loads(str(self._probe.create_event(addon_name))) + self._probe.create_event(addon_name, + reply_handler=save_args(self._event_created_cb, event_created_cb), + error_handler=ignore) + + def _event_created_cb(self, event_created_cb, event): + LOGGER.debug("ProbeProxy :: _event_created_cb, calling upper layer") + event = pickle.loads(str(event)) + event_created_cb(event) def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): @@ -751,20 +866,33 @@ class ProbeManager(object): self._probes = {} self._current_activity = None + self.list_pending_actions = [] + self.list_action_installed_cb = [] + self.list_error_cb = [] + + self.list_pending_transitions = [] + self.list_notification_cb = [] + self.list_event_subscribed_cb = [] + self.list_error_cb = [] + + self.is_activity_launching = False + ProbeManager._LOGGER.debug("__init__()") def setCurrentActivity(self, activity_id): - if not activity_id in self._probes: - raise RuntimeError("Activity not attached, id : %s"%activity_id) + # HACK : Disabling check for now, since it prevents usage of probes + # in activities that have yet to register their probes... We might + # set the current activity before having to execute anything inside it + # e.g. A new source is crawling in and we need to start the activity + # + # This should be removed once the Home Window probes are installed. + + #if not activity_id in self._probes: + # raise RuntimeError("Activity not attached, id : %s"%activity_id) + LOGGER.debug("ProbeManager :: New activity set as current = %s", str(activity_id)) self._current_activity = activity_id def getCurrentActivity(self): - # TODO : Insert the correct call to remember the current activity, - # taking the views and frame into account - current_act = get_model().get_active_activity() - current_act_bundle = ActivityBundle(current_act.get_bundle_path()) - current_act_id = current_act_bundle.get_bundle_id() - self._current_activity = current_act_id return self._current_activity currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) @@ -775,6 +903,43 @@ class ProbeManager(object): else: return None + def prelaunch_activity(self, activity, action_event, is_event=False): + if activity == "org.sugar.desktop.mesh": + get_model()._set_zoom_level(get_model().ZOOM_MESH) + return False + elif activity == "org.sugar.desktop.group": + get_model()._set_zoom_level(get_model().ZOOM_GROUP) + return False + elif activity == "org.sugar.desktop.home": + get_model()._set_zoom_level(get_model().ZOOM_HOME) + return False + + if activity == get_model().get_active_activity().get_type(): + return False + + model = get_model() + for active_activity in model: + if active_activity is not None and active_activity.get_type() == activity: + active_activity.get_window().activate(gtk.get_current_event_time()) + return False + + bundle = bundleregistry.get_registry().get_bundle(activity) + if not bundle: + print 'WARNING : Cannot find bundle' + else: + path = bundle.get_path() + activity_bundle = ActivityBundle(path) + if self.is_activity_launching == False: + activityfactory.create(activity_bundle) + self.is_activity_launching = True + + if is_event: + self.list_pending_transitions.append(action_event) + else: + self.list_pending_actions.append(action_event) + return True + return False + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): """ Install an action on the current activity @@ -792,6 +957,13 @@ class ProbeManager(object): activity = self.currentActivity if activity: + wait_install = self.prelaunch_activity(activity, action) + + if wait_install: + self.list_action_installed_cb.append(action_installed_cb) + self.list_error_cb.append(error_cb) + return + return self._first_proxy(activity).install( action=action, is_editing=is_editing, @@ -840,16 +1012,17 @@ class ProbeManager(object): else: raise RuntimeWarning("No activity attached") - def create_event(self, addon_name): + def create_event(self, addon_name, event_created_cb): """ Create an event on the app side and request the user to fill the properties before returning it. @param addon_name: the add-on name of the event + @param event_created_cb The notification to send once the event was created @returns: an eventfilter instance """ if self.currentActivity: - return self._first_proxy(self.currentActivity).create_event(addon_name) + return self._first_proxy(self.currentActivity).create_event(addon_name, event_created_cb) else: raise RuntimeWarning("No activity attached") @@ -867,6 +1040,14 @@ class ProbeManager(object): activity = self.get_source_activity(event) if activity: + wait_install = self.prelaunch_activity(activity, event, True) + + if wait_install: + self.list_notification_cb.append(notification_cb) + self.list_event_subscribed_cb.append(event_subscribed_cb) + self.list_error_cb.append(error_cb) + return + return self._first_proxy(activity).subscribe(event, notification_cb,\ event_subscribed_cb, error_cb) else: @@ -906,11 +1087,47 @@ class ProbeManager(object): process_name = str(process_name) unique_id = str(unique_id) ProbeManager._LOGGER.debug("register_probe(%s,%s)", process_name, unique_id) + if process_name not in self._probes: self._probes[process_name] = [(unique_id,self._ProxyClass(process_name, unique_id))] else: self._probes[process_name].append((unique_id,self._ProxyClass(process_name, unique_id))) + # Register the probe that was just installed as the current activity + # (this will be true by default since we probably were waiting for it + # to open up) + self.currentActivity = process_name + cnt_action = 0 + for pending_action in self.list_pending_actions: + self._first_proxy(self.currentActivity).install( + action=pending_action, + is_editing=False, + action_installed_cb=self.list_action_installed_cb[cnt_action], + error_cb=self.list_error_cb[cnt_action], + editing_cb=False + ) + cnt_action = cnt_action + 1 + + cnt_transition = 0 + for pending_transition in self.list_pending_transitions: + self._first_proxy(self.currentActivity).subscribe( + pending_transition, + self.list_notification_cb[cnt_transition], + self.list_event_subscribed_cb[cnt_transition], + self.list_error_cb[cnt_transition] + ) + cnt_transition = cnt_transition + 1 + + self.list_pending_actions = [] + self.list_action_installed_cb = [] + self.list_error_cb = [] + + self.list_pending_transitions = [] + self.list_notification_cb = [] + self.list_event_subscribed_cb = [] + self.list_error_cb = [] + + self.is_activity_launching = False def unregister_probe(self, unique_id): """ Remove a probe from the known probes. diff --git a/tutorius/addon.py b/tutorius/addon.py index ca729ae..1fd5143 100644 --- a/tutorius/addon.py +++ b/tutorius/addon.py @@ -78,9 +78,16 @@ def create(name, *args, **kwargs): comp_metadata = _cache[name] try: return comp_metadata['class'](*args, **kwargs) - except: - logging.debug("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs))) - return None + except Exception, e: + LOGGER.error("Could not instantiate %s with parameters %s, %s"%\ + (comp_metadata['name'],str(args), str(kwargs))) + + # fetch frame information to complement exception with traceback + type, value, tb = sys.exc_info() + formatted_tb = traceback.format_tb(tb) + LOGGER.error('Error loadin tutorius add-on named [%s]:\n%s\n%s' % \ + (name, '\n'.join(formatted_tb), str(e))) + return None except KeyError: logging.debug("Addon not found for class '%s'", name) return None diff --git a/tutorius/creator.py b/tutorius/creator.py index 6ba7011..92344d2 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -32,7 +32,7 @@ import os from sugar.graphics import icon, style import jarabe.frame -from . import overlayer, gtkutils, vault, addon +from . import overlayer, gtkutils, vault, addon, translator from .tutorial import Tutorial from . import viewer from .propwidgets import TextInputDialog @@ -91,7 +91,7 @@ class Creator(Object): if Creator._instance: raise RuntimeError("Creator was already instanciated") Creator._instance = self - self._probe_mgr = probe_manager + self._probe_mgr_unwrapped = probe_manager self._installed_actions = list() def start_authoring(self, tutorial=None): @@ -130,8 +130,23 @@ class Creator(Object): self._selected_widget = None self._eventmenu = None self.tuto = None - self._guid = None - self.metadata = None + + self._guid = str(uuid.uuid1()) + self._metadata = { + vault.INI_GUID_PROPERTY: self._guid, + vault.INI_NAME_PROPERTY: '', + vault.INI_VERSION_PROPERTY: '1', + } + + related_activities_dict = {} + self._metadata['activities'] = dict(related_activities_dict) + # Save Tutorial right now, so resource can be added right now. + # If the tutorial is still unnamed at quit it will be removed. + vault.Vault.saveTutorial(self._tutorial, self._metadata) + + self._probe_mgr = translator.ResourceTranslator( + self._probe_mgr_unwrapped, + self._guid) frame = jarabe.frame.get_view() @@ -306,8 +321,17 @@ class Creator(Object): """ event_type = self._propedit.events_list[path][ToolBox.ICON_NAME] - event = self._probe_mgr.create_event(event_type) + event = self._probe_mgr.create_event(event_type, + event_created_cb=partial(self._event_created, event_type)) + def _event_created(self, event_type, event): + """ + Callback to execute when the creation of a new event is complete. + + @param event_type The type of event that was created + @param event The event that was instanciated + """ + LOGGER.debug("Creator :: _event_created, now setting source and adding inside tutorial") # Configure the event prior to installing it # Currently, this consists of writing its source event.source = self._probe_mgr.currentActivity @@ -393,13 +417,18 @@ class Creator(Object): self._overview.destroy() self.is_authoring = False + # remove unsaved tutorial remains + if not self._metadata[vault.INI_NAME_PROPERTY]: + LOGGER.debug("Creator :: removing unsaved tutorial %s" % \ + str(self._guid)) + vault.Vault.deleteTutorial(self._guid) + def save(self, widget=None): """ Save the currently edited tutorial to bundle, prompting for a name as needed. """ - if not self._guid: - self._guid = str(uuid.uuid1()) + if not self._metadata[vault.INI_NAME_PROPERTY]: dlg = TextInputDialog(parent=self._overview.win, text=T("Enter a tutorial title."), field=T("Title")) @@ -432,6 +461,7 @@ class Creator(Object): related_activities_dict[activity_name] = str(bundle.get_activity_version()) self._metadata['activities'] = dict(related_activities_dict) + self._metadata[vault.INI_NAME_PROPERTY] = tutorial_name vault.Vault.saveTutorial(self._tutorial, self._metadata) @@ -465,6 +495,24 @@ class Creator(Object): """ return self.is_authoring + def set_resource(self, resource_id, file_path): + """ + Adds a resource to the currently edited tutorial. + This is intended for use by the property widgets to update resources + without knowing anything about the vault and tutorial GUID. + + @param resource_id: the id of the resource to update, or an empty + string for a new resource. + @param file_path: the path to the resource to use for update. + @returns the new resource_id + """ + LOGGER.debug("Creator :: Updating resource from '%s'" % file_path) + + if resource_id: + vault.Vault.delete_resource(self._guid, resource_id) + + return str(vault.Vault.add_resource(self._guid, file_path)) + def update_addon_property(self, addon_address, diff_dict): """ Updates the properties on an addon. diff --git a/tutorius/engine.py b/tutorius/engine.py index 39cfeeb..95aefe7 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -108,7 +108,7 @@ class TutorialRunner(object): # Verify if we just completed the subscription of all the events for this state self._verify_event_install_state() - def subscribe_error(self, event_name, exception): + def subscribe_error(self, event_name, event, exception): # TODO : Do correct error handling here LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : %s"%(event_name, str(exception))) self._subscription_errors[event_name] = exception @@ -309,6 +309,10 @@ class Engine: """ Launch a tutorial @param tutorialID unique tutorial identifier used to retrieve it from the disk """ + + if self._probeManager.is_activity_launching: + return + if self._tutorial: self.stop() @@ -317,10 +321,15 @@ class Engine: #Get the active activity from the shell activity = self._shell.get_active_activity() - #TProbes automatically use the bundle id, available from the ActivityBundle - bundle = ActivityBundle(activity.get_bundle_path()) - self._tutorial._activity_id = bundle.get_bundle_id() #HACK until we have activity id's in action/events + LOGGER.debug("Engine :: Launching tutorial on activity %s", activity.get_type()) + if hasattr(activity, 'is_journal') and activity.is_journal(): + self._tutorial._activity_id = 'org.laptop.JournalActivity' + else: + #TProbes automatically use the bundle id, available from the ActivityBundle + bundle = ActivityBundle(activity.get_bundle_path()) + + self._tutorial._activity_id = bundle.get_bundle_id() #HACK until we have activity id's in action/events self._tutorial.start() diff --git a/tutorius/localization.py b/tutorius/localization.py new file mode 100644 index 0000000..3a9d40a --- /dev/null +++ b/tutorius/localization.py @@ -0,0 +1,68 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@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 os + +class LocalizationHelper(object): + + @classmethod + def _write_addon_strings_to_file(cls, addon, output_file): + """ + For a given addon, writes a pot file entry for every string property + it has. + + @param addon The addon from which we want to get the string properties + @param output_file The file in which we should write the strings + @return None + """ + for (prop_name, prop_value) in addon._props.items(): + prop_type = getattr(type(addon), prop_name).type + if prop_type == "string" and prop_value: + prop_value = prop_value.replace("\n", "\\n") + prop_value = prop_value.replace("\r", "\\r") + output_file.write('msgid "%s"\nmsgstr ""\n\n'%(prop_value)) + + @classmethod + def write_translation_file(cls, tutorial, output_file): + """ + Writes the translation file to the given file, according to the .pot + files specifications, for the given tutorial. + + This will generate a pair of line for each TStringProperty in the following + format : + msgid "<string>" + msgstr "" + + This will enable the translator to create a localization for this tutorial. + + @param tutorial The executable reprensentation of the tutorial + @param output_file An opened file object to which the strings translation + template will be written + @return Nothing + """ + state_dict = tutorial.get_state_dict() + + for state_name in state_dict.keys(): + actions = tutorial.get_action_dict(state_name) + events = tutorial.get_transition_dict(state_name) + + for action in actions.values(): + cls._write_addon_strings_to_file(action, output_file) + + for (event, next_state) in events.values(): + cls._write_addon_strings_to_file(event, output_file) + diff --git a/tutorius/properties.py b/tutorius/properties.py index a0d63bb..1905117 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -36,7 +36,9 @@ from .propwidgets import PropWidget, \ EventTypePropWidget, \ IntPropWidget, \ FloatPropWidget, \ - IntArrayPropWidget + IntArrayPropWidget, \ + ResourcePropWidget, \ + ScreenClipPropWidget import logging LOGGER = logging.getLogger("properties") @@ -372,6 +374,9 @@ class TResourceProperty(TutoriusProperty): TFileProperties with absolute paths, so that they can be used on any machine. """ + + widget_class = ResourcePropWidget + def __init__(self, resource_name=""): """ Creates a new resource pointing to an existing resource. @@ -386,6 +391,26 @@ class TResourceProperty(TutoriusProperty): self.default = self.validate("") +class TScreenClipProperty(TResourceProperty): + """ + Represents an image resource from a screen capture + + 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. + """ + + widget_class = ScreenClipPropWidget + + def __init__(self, *args, **kwargs): + """ + 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) + """ + super(TScreenClipProperty, self).__init__(*args, **kwargs) + class TEnumProperty(TutoriusProperty): """ Represents a value in a given enumeration. This means that the value will diff --git a/tutorius/propwidgets.py b/tutorius/propwidgets.py index dfc6ac0..0e6c200 100644 --- a/tutorius/propwidgets.py +++ b/tutorius/propwidgets.py @@ -20,6 +20,15 @@ Allows displaying properties cleanly. """ import gtk import gobject +from jarabe.journal.objectchooser import ObjectChooser +from sugar.datastore.datastore import DSObject +from sugar import mime +import uuid +import tempfile +import os + +import logging +LOGGER = logging.getLogger("sugar.tutorius.propwidgets") from . import gtkutils, overlayer ########################################################################### @@ -94,6 +103,7 @@ class SignalInputDialog(gtk.MessageDialog): iter = self.entry.get_active_iter() if iter: text = self.model.get_value(iter, 0) + LOGGER.debug("SignalInputDialog :: Got signal name %s", text) return text return None @@ -494,3 +504,179 @@ class IntArrayPropWidget(PropWidget): @param propname name of property to edit """ pass + +class ResourcePropWidget(PropWidget): + """Allows adding and changing tutorial resources.""" + + def _chooser_response_cb(self, chooser, response_id, chooser_id, widget): + """ + Callback for receiving file choices. + """ + if response_id == gtk.RESPONSE_ACCEPT: + object_id = chooser.get_selected_object_id() + jobject = DSObject(object_id=object_id) + + from . import creator + res_path = str(jobject.file_path) + + creator_obj = creator.default_creator() + resource_id = creator_obj.set_resource(self.obj_prop, res_path) + self.widget.set_label(self.obj_prop) + jobject.destroy() + + self.obj_prop = resource_id + self.notify() + + chooser.destroy() + del chooser + + def _show_file_chooser(self, widget): + """ + Select a resource and add it through the creator. + This is expected to run in the same process, alongside the creator. + """ + chooser_id = uuid.uuid4().hex + chooser = ObjectChooser(self._parent, + what_filter=mime.GENERIC_TYPE_IMAGE) + chooser.connect('response', self._chooser_response_cb, + chooser_id, widget) + chooser.show() + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + propwdg = gtk.Button(init_value) + propwdg.connect_after("clicked", self._show_file_chooser) + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + if self.obj_prop: + self.widget.set_label(self.obj_prop) + else: + self.widget.set_label("") + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + raise RuntimeError('Cannot select a default resource') + +class ScreenClipPropWidget(PropWidget): + """Allows adding and changing tutorial resources.""" + def _on_drag_end(self, widget, event, pixbuf): + from . import creator + widget.destroy() + + end_x, end_y = event.get_coords() + width = abs(end_x - self.start_x) + height = abs(end_y - self.start_y) + x_off = min(self.start_x, end_x) + y_off = min(self.start_y, end_y) + + cropped = pixbuf.subpixbuf(x_off, y_off, width, height) + + tmp_name = tempfile.mktemp(suffix='.png') + try: + cropped.save(tmp_name, 'png') + creator_obj = creator.default_creator() + resource_id = creator_obj.set_resource(self.obj_prop, tmp_name) + self.obj_prop = resource_id + finally: + os.unlink(tmp_name) + + self.notify() + + def _on_drag_start(self, widget, event, pixbuf): + widget.connect('button-release-event', self._on_drag_end, pixbuf) + widget.connect('motion-notify-event', self._on_drag_move, pixbuf) + self.start_x, self.start_y = event.get_coords() + + def _on_drag_move(self, widget, event, pixbuf): + if gtk.gdk.events_pending(): + return + + end_x, end_y = event.get_coords() + width = abs(end_x - self.start_x) + height = abs(end_y - self.start_y) + x_off = min(self.start_x, end_x) + y_off = min(self.start_y, end_y) + + ctx = widget.window.cairo_create() + ctx.set_source_pixbuf(pixbuf, 0, 0) + ctx.paint() + + ctx.set_source_rgb(0, 0, 0) + ctx.rectangle(x_off, y_off, width, height) + ctx.stroke() + + def _get_capture(self, widget): + """ + Select a resource and add it through the creator. + This is expected to run in the same process, alongside the creator. + """ + # take screen capture + root = gtk.gdk.get_default_root_window() + width, height = root.get_size() + pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, + width, height) + pixbuf.get_from_drawable(src=root, + cmap=gtk.gdk.colormap_get_system(), + src_x=0, src_y=0, + dest_x=0, dest_y=0, + width=width, height=height) + + win = gtk.Window() + image = gtk.Image() + image.set_from_pixbuf(pixbuf) + win.add(image) + win.show_all() + win.set_app_paintable(True) + win.fullscreen() + win.present() + win.add_events(gtk.gdk.BUTTON_PRESS_MASK | \ + gtk.gdk.BUTTON_RELEASE_MASK | \ + gtk.gdk.POINTER_MOTION_MASK) + win.connect('button-press-event', self._on_drag_start, pixbuf) + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + propwdg = gtk.Button("Clip Screen") + propwdg.connect_after("clicked", self._get_capture) + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + # Nothing to refresh + pass + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + # TODO We're assuming all reasource creation is done from the creator + # and not from the probe since there is a requirement to know the guid + # to add resources. But for this resource type, this could technically + # be done in the probe. + raise RuntimeError('Cannot select a default resource') diff --git a/tutorius/service.py b/tutorius/service.py index 1564339..97d914b 100644 --- a/tutorius/service.py +++ b/tutorius/service.py @@ -87,6 +87,11 @@ class Service(dbus.service.Object): LOGGER.debug("Service.unregister_probe(%s)", unique_id) self._probeMgr.unregister_probe(unique_id) + @dbus.service.method(_DBUS_SERVICE_IFACE, + in_signature='s', out_signature="") + def set_current_act(self, bundle_id): + self._probeMgr.currentActivity = str(bundle_id) + class ServiceProxy: """ Proxy to connect to the Service object, abstracting the DBus interface""" @@ -137,6 +142,9 @@ class ServiceProxy: # asynchronous call to be completed self._service.unregister_probe(unique_id) + def set_current_act(self, bundle_id): + remote_call(self._service.set_current_act, (bundle_id,), block=False) + if __name__ == "__main__": import dbus.mainloop.glib diff --git a/tutorius/translator.py b/tutorius/translator.py index f1c088b..ee1067b 100644 --- a/tutorius/translator.py +++ b/tutorius/translator.py @@ -17,6 +17,9 @@ import os import logging import copy as copy_module +import gettext +import os +import locale logger = logging.getLogger("ResourceTranslator") @@ -52,7 +55,47 @@ class ResourceTranslator(object): """ self._probe_manager = probe_manager self._tutorial_id = tutorial_id + + # Pick up the language for the user + langs = [] + language = os.environ.get("LANGUAGE", None) + if language: + langs = language.split(':') + + # Get the default machine language + lc, encoding = locale.getdefaultlocale() + if lc: + langs += [lc] + + l10n_dir = Vault.get_localization_dir(tutorial_id) + logger.debug("ResourceTranslator :: Looking for localization resources for languages %s in folder %s"%(str(langs), l10n_dir)) + if l10n_dir: + try: + trans = gettext.translation('tutorial_text', + l10n_dir, + languages=langs) + self._ = trans.ugettext + except IOError: + self._ = None + else: + self._ = None + def translate_string(self, str_value): + """ + Replaces the TString property by its localized equivalent. + + @param str_value The straing to translate + """ + # If we have a localization folder + if self._: + # Apply the translation + u_string = unicode(self._(str_value)) + + # Encode the string in UTF-8 for it to pass thru DBus + return u_string.encode("utf-8") + # There was no translation + return unicode(str_value).encode("utf-8") + def translate_resource(self, res_value): """ Replace the TResourceProperty in the container by their @@ -62,18 +105,15 @@ class ResourceTranslator(object): 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 + @param res_value The resource property's value to be translated @return The TFileProperty corresponding to this resource, containing - an absolute path to the resource + an absolute path to it """ # 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 + return filepath def translate(self, prop_container): """ @@ -100,6 +140,12 @@ class ResourceTranslator(object): prop_value = getattr(prop_container, propname) prop_type = getattr(type(prop_container), propname).type + # If the propert is a string, we need to use the localization + # to find it's equivalent + if prop_type == "string": + str_value = self.translate_string(prop_value) + prop_container.replace_property(propname, str_value) + # If the property is a resource, then we need to query the # vault to create its correspondent if prop_type == "resource": @@ -150,12 +196,6 @@ class ResourceTranslator(object): def detach(self, activity_id): self._probe_manager.detach(activity_id) - def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): - return self._probe_manager.subscribe(event, notification_cb, event_subscribed_cb, error_cb) - - 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) @@ -165,6 +205,8 @@ class ResourceTranslator(object): def get_registered_probes_list(self, process_name=None): return self._probe_manager.get_registered_probes_list(process_name) + def create_event(self, *args, **kwargs): + return self._probe_manager.create_event(*args, **kwargs) ########################################################################### def action_installed(self, action_installed_cb, address): @@ -186,17 +228,39 @@ class ResourceTranslator(object): self.translate(new_action) # Send the new action to the probe manager - self._probe_manager.install(new_action, save_args(self.action_installed, action_installed_cb), + self._probe_manager.install(new_action, + save_args(self.action_installed, action_installed_cb), save_args(self.action_install_error, error_cb, new_action), is_editing=is_editing, editing_cb=editing_cb) - def update(self, action_address, newaction): + def update(self, action_address, newaction, is_editing=False): translated_new_action = copy_module.deepcopy(newaction) self.translate(translated_new_action) - self._probe_manager.update(action_address, translated_new_action, block) + self._probe_manager.update(action_address, translated_new_action, is_editing) - def uninstall(self, action_address): + def uninstall(self, action_address, is_editing=False): self._probe_manager.uninstall(action_address) + def subscribe_complete_cb(self, event_subscribed_cb, event, address): + event_subscribed_cb(address) + + def event_subscribe_error(self, error_cb, event, exception): + error_cb(event, exception) + + def translator_notification_cb(self, event, notification_cb, new_event): + notification_cb(event) + + def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): + new_event = copy_module.deepcopy(event) + self.translate(new_event) + + self._probe_manager.subscribe(new_event, + save_args(self.translator_notification_cb, event, notification_cb), + save_args(self.subscribe_complete_cb, event_subscribed_cb, event), + save_args(self.event_subscribe_error, error_cb)) + + def unsubscribe(self, address): + return self._probe_manager.unsubscribe(address) + diff --git a/tutorius/vault.py b/tutorius/vault.py index 2b9c5b9..15c7e17 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -32,6 +32,7 @@ from ConfigParser import SafeConfigParser from . import addon from .tutorial import Tutorial, State, AutomaticTransitionEvent +from localization import LocalizationHelper logger = logging.getLogger("tutorius") @@ -60,6 +61,7 @@ INI_CATEGORY_PROPERTY = 'category' INI_FILENAME = "meta.ini" TUTORIAL_FILENAME = "tutorial.xml" RESOURCES_FOLDER = 'resources' +LOCALIZATION_FOLDER = 'localization' ###################################################################### # XML Tag names and attributes @@ -327,35 +329,41 @@ class Vault(object): # 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) - with open(os.path.join(tutorial_path, TUTORIAL_FILENAME), 'w') as fsmfile: - xml_ser.save_tutorial(tutorial, fsmfile) - - # 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) + # Serialize the tutorial and write it to disk + xml_ser = XMLSerializer() - # Write the file to disk - with open(ini_file_path, 'wb') as configfile: - parser.write(configfile) + with open(os.path.join(tutorial_path, TUTORIAL_FILENAME), 'w') as fsmfile: + xml_ser.save_tutorial(tutorial, fsmfile) + + # 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) + l10n_path = os.path.join(tutorial_path, LOCALIZATION_FOLDER) + try: + os.mkdir(l10n_path) + except: + # FIXME : Ignore error as we suppose it is + # the directory already exists + pass + # Write the localization template (.pot) file + with open(os.path.join(l10n_path, 'tutorial_text.pot'), "wb") as l10n_file: + LocalizationHelper.write_translation_file(tutorial, l10n_file) - else: - # Error, tutorial already exist - return False - @staticmethod def deleteTutorial(Guid): @@ -430,15 +438,28 @@ class Vault(object): for root, dirs, files in os.walk(os.path.join(bundle_path, RESOURCES_FOLDER)): for name in files: archive_list.append(os.path.join(bundle_path, RESOURCES_FOLDER, name)) + + logger.debug("Vault :: Looking for translation .mo files...") + + for root, dirs, files in os.walk(os.path.join(bundle_path, LOCALIZATION_FOLDER)): + logger.debug("Vault :: Inspecting files %s at root %s", str(files), root) + for name in files: + if name == "tutorial_text.mo": + fname_splitted = root.rsplit('/') + archive_list.append(os.path.join(bundle_path, LOCALIZATION_FOLDER, fname_splitted[-2], fname_splitted[-1], name)) zfilename = str(guid) + ".zip" zout = zipfile.ZipFile(os.path.join(bundle_path, zfilename), "w") for fname in archive_list: + logger.debug("Vault :: zipping file %s"%fname) fname_splitted = fname.rsplit('/') if fname_splitted[-2] == RESOURCES_FOLDER: ressource_path_and_file = os.path.join(fname_splitted[-2], fname_splitted[-1]) zout.write(fname, ressource_path_and_file) + elif len(fname_splitted) >= 4 and fname_splitted[-4] == LOCALIZATION_FOLDER: + translation_path_and_file = os.path.join(*fname_splitted[-4:]) + zout.write(fname, translation_path_and_file) else: file_only_name = fname_splitted[-1] zout.write(fname, file_only_name) @@ -528,6 +549,25 @@ class Vault(object): else: return None + @staticmethod + def get_localization_dir(tutorial_guid): + """ + Returns the base folder under which all the <language>/LC_MESSAGES/tutorial_text.mo + are stored. These files are used for runtime translations by the Resource + Translator. + + @param tutorial_guid the guid of the tutorial + @returns the directory that stores the translation objects + """ + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Check if the localization directory exists + l10n_dir = os.path.join(tutorial_path, LOCALIZATION_FOLDER) + if os.path.isdir(l10n_dir): + return l10n_dir + else: + return None class Serializer(object): """ |