From a1e4d136f860b03abcafc7bf2e2d65b412bc13cd Mon Sep 17 00:00:00 2001 From: JCTutorius Date: Sun, 27 Dec 2009 19:32:14 +0000 Subject: Merge branch 'master' of gitorious@git.sugarlabs.org:tutorius/mainline --- diff --git a/Workshop.activity/Rating.py b/Workshop.activity/Rating.py index a13e5a2..684175b 100644 --- a/Workshop.activity/Rating.py +++ b/Workshop.activity/Rating.py @@ -1,3 +1,18 @@ +# 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 from gtk import gdk import logging @@ -141,4 +156,4 @@ class Rating(gtk.Widget): if (self.rating > 5): self.rating = 5 # redraw the widget - self.queue_draw() \ No newline at end of file + self.queue_draw() diff --git a/Workshop.activity/TutorialStoreCategories.py b/Workshop.activity/TutorialStoreCategories.py index c321d66..aa3b4c6 100644 --- a/Workshop.activity/TutorialStoreCategories.py +++ b/Workshop.activity/TutorialStoreCategories.py @@ -1,3 +1,18 @@ +# 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 sys, os import gtk diff --git a/Workshop.activity/TutorialStoreDetails.py b/Workshop.activity/TutorialStoreDetails.py index 83c5366..3b8fc89 100644 --- a/Workshop.activity/TutorialStoreDetails.py +++ b/Workshop.activity/TutorialStoreDetails.py @@ -1,3 +1,18 @@ +# 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 sys, os import gtk from Workshop import WorkshopDetail @@ -5,7 +20,7 @@ from Workshop import WorkshopDetail class TutorialStoreDetails(WorkshopDetail): def __init__(self,tutorial,controller): - WorkshopDetail.__init__(self,tutorial,controller) + WorkshopDetail.__init__(self,tutorial,controller,True) download_button = gtk.Button('Download') infos_button = gtk.Button('Informations') diff --git a/Workshop.activity/TutorialStoreHome.py b/Workshop.activity/TutorialStoreHome.py index a9051e7..5fce564 100644 --- a/Workshop.activity/TutorialStoreHome.py +++ b/Workshop.activity/TutorialStoreHome.py @@ -1,3 +1,18 @@ +# 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 logging import TutorialStoreCategories import TutorialStoreSearch diff --git a/Workshop.activity/TutorialStoreResults.py b/Workshop.activity/TutorialStoreResults.py index 3a7f78d..a6bda76 100644 --- a/Workshop.activity/TutorialStoreResults.py +++ b/Workshop.activity/TutorialStoreResults.py @@ -1,3 +1,18 @@ +# 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 sys, os import gtk from Workshop import WorkshopListItem @@ -106,7 +121,7 @@ class TutorialStoreResults(gtk.VBox): class TutorialStoreListItem(WorkshopListItem): def __init__(self,tutorial,controller): - WorkshopListItem.__init__(self,tutorial,controller) + WorkshopListItem.__init__(self,tutorial,controller,True) self.last_row = gtk.HBox(False,15) self.btn_detail = gtk.Button('Details') @@ -117,4 +132,4 @@ class TutorialStoreListItem(WorkshopListItem): self.last_row.show_all() #connect the buttons - self.btn_detail.connect("clicked",self.controller.show_details,self.tutorial) \ No newline at end of file + self.btn_detail.connect("clicked",self.controller.show_details,self.tutorial) diff --git a/Workshop.activity/TutorialStoreSearch.py b/Workshop.activity/TutorialStoreSearch.py index 4303a07..9007599 100644 --- a/Workshop.activity/TutorialStoreSearch.py +++ b/Workshop.activity/TutorialStoreSearch.py @@ -1,3 +1,18 @@ +# 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 sys, os import gtk @@ -35,4 +50,4 @@ class TutorialStoreSearch(gtk.HBox): for category in categories: self.search_combobox.append_text(category) - \ No newline at end of file + diff --git a/Workshop.activity/TutorialStoreSuggestion.py b/Workshop.activity/TutorialStoreSuggestion.py index 45fc1c3..a1c7e45 100644 --- a/Workshop.activity/TutorialStoreSuggestion.py +++ b/Workshop.activity/TutorialStoreSuggestion.py @@ -1,3 +1,18 @@ +# 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 sys, os import gtk @@ -77,4 +92,4 @@ class SuggestionListItem(gtk.Frame): self.add(self.container) #tutorial5_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0)) - self.container.show_all() \ No newline at end of file + self.container.show_all() diff --git a/Workshop.activity/TutoriusActivity.py b/Workshop.activity/TutoriusActivity.py index f2f3adc..c3a228c 100755 --- a/Workshop.activity/TutoriusActivity.py +++ b/Workshop.activity/TutoriusActivity.py @@ -55,10 +55,11 @@ class TutoriusActivity(activity.Activity): toolbox.show() self.table = gtk.VPaned() - self.table.set_position(500) self.left_container = gtk.HBox() btn1 = gtk.Button("My tutorials") + btn1.set_size_request(120, 10) btn2 = gtk.Button("Tutorial Store") + btn2.set_size_request(120, 10) self.left_container.pack_start(btn1,expand=False) self.left_container.pack_start(btn2,expand=False) @@ -81,8 +82,8 @@ class TutoriusActivity(activity.Activity): self.model.query(None) - self.table.add2(self.left_container) - self.table.add1(self.workshop_my_tutorial) + self.table.pack2(self.left_container, False, True) + self.table.pack1(self.workshop_my_tutorial, False, True) self.set_canvas(self.table) self.workshop_store.show() diff --git a/Workshop.activity/Workshop.py b/Workshop.activity/Workshop.py index 857bf8c..09ac402 100644 --- a/Workshop.activity/Workshop.py +++ b/Workshop.activity/Workshop.py @@ -257,7 +257,7 @@ class SearchBar(gtk.HBox): self.sort_combo.connect("changed",self.controller.sort_selection_changed,None) class WorkshopDetail(gtk.VBox): - def __init__(self,tutorial,controller): + def __init__(self,tutorial,controller,rating_editable=False): """ Constructor @@ -302,7 +302,7 @@ class WorkshopDetail(gtk.VBox): label_holder.pack_start(self.title_label) label_holder.pack_start(self.author_label) - self.rating = Rating(tutorial,controller,rating = tutorial.rating) + self.rating = Rating(tutorial,controller,tutorial.rating,rating_editable) second_row = gtk.HBox(False) second_row.pack_start(icon,False,False) @@ -359,7 +359,7 @@ class WorkshopListItem(gtk.Alignment): """ A list item containing the details of a tutorial """ - def __init__(self,tutorial,controller): + def __init__(self,tutorial,controller,rating_editable=False): """ Constructor @@ -388,7 +388,7 @@ class WorkshopListItem(gtk.Alignment): self.icon = gtk.Image() self.icon.set_from_file('icon.svg') - self.rating = Rating(tutorial,controller,tutorial.rating, True) + self.rating = Rating(tutorial,controller,tutorial.rating,rating_editable) #Add the controls to the table self.table.attach(self.icon,0,1,0,1,0,0) @@ -408,7 +408,7 @@ class WorkshopListItem(gtk.Alignment): class MyTutorialListItem(WorkshopListItem): def __init__(self,tutorial,controller): - WorkshopListItem.__init__(self,tutorial,controller) + WorkshopListItem.__init__(self,tutorial,controller,False) self.last_row = gtk.HBox(False,15) self.btn_launch = gtk.Button('Launch') @@ -427,7 +427,7 @@ class MyTutorialListItem(WorkshopListItem): class MyTutorialDetail(WorkshopDetail): def __init__(self,tutorial,controller): - WorkshopDetail.__init__(self,tutorial,controller) + WorkshopDetail.__init__(self,tutorial,controller,False) #The bottom of the screen contains the button(fourth and fifth row self.launch_button = gtk.Button('Launch') diff --git a/Workshop.activity/WorkshopController.py b/Workshop.activity/WorkshopController.py index 84b5999..8d7fca2 100644 --- a/Workshop.activity/WorkshopController.py +++ b/Workshop.activity/WorkshopController.py @@ -1,3 +1,18 @@ +# 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 """ WorkshopController diff --git a/Workshop.activity/WorkshopModel.py b/Workshop.activity/WorkshopModel.py index c52ab5b..9d2b804 100644 --- a/Workshop.activity/WorkshopModel.py +++ b/Workshop.activity/WorkshopModel.py @@ -171,7 +171,10 @@ class WorkshopModel(): @param tutorial The tutorial to launch """ - pass + from sugar.tutorius.service import ServiceProxy + service = ServiceProxy() + + service.launch(tutorial.id) @Login(store_proxy,login_view) def rate_tutorial(self,tutorial,rating): diff --git a/addons/bubblemessagewimg.py b/addons/bubblemessagewimg.py index 974dd19..2f6c36f 100644 --- a/addons/bubblemessagewimg.py +++ b/addons/bubblemessagewimg.py @@ -23,7 +23,7 @@ class BubbleMessageWImg(Action): position = TArrayProperty((0,0), 2, 2) # Do the same for the tail position tail_pos = TArrayProperty((0,0), 2, 2) - imgpath = TResourceProperty("") + imgpath = TResourceProperty('') def __init__(self, **kwargs): """ @@ -61,7 +61,7 @@ class BubbleMessageWImg(Action): # TODO: tails are relative to tailpos. They should be relative to # the speaking widget. Same of the bubble position. self._bubble = TextBubbleWImg(text=self.message, - tailpos=self.tail_pos,imagepath=self.imgpath.default) + tailpos=self.tail_pos,imagepath=self.imgpath) self._bubble.show() self.overlay.put(self._bubble, x, y) self.overlay.queue_draw() @@ -87,17 +87,21 @@ class BubbleMessageWImg(Action): self.overlay = overlayer assert not self._drag, "bubble action set to editmode twice" x, y = self.position - self._bubble = TextBubbleWImg(text=self.message, - tailpos=self.tail_pos,imagepath=self.imgpath) - self.overlay.put(self._bubble, x, y) - self._bubble.show() + if self.imgpath: + self._bubble = TextBubbleWImg(text=self.message, + tailpos=self.tail_pos,imagepath=self.imgpath) + self.overlay.put(self._bubble, x, y) + self._bubble.show() - self._drag = DragWrapper(self._bubble, self.position, update_action_cb=self.update_property, draggable=True) + self._drag = DragWrapper(self._bubble, self.position, + update_action_cb=self.update_property, + draggable=True) def exit_editmode(self, *args): - x,y = self._drag.position - self.position = (int(x), int(y)) if self._drag: + x,y = self._drag.position + self.position = (int(x), int(y)) + self._drag.draggable = False self._drag = None if self._bubble: @@ -110,6 +114,6 @@ __action__ = { "display_name" : "Message Bubble with image", "icon" : "message-bubble", "class" : BubbleMessageWImg, - "mandatory_props" : ["message",'imgpath'] + "mandatory_props" : ["message"] } diff --git a/addons/dialogmessage.py b/addons/dialogmessage.py deleted file mode 100644 index 24646f8..0000000 --- a/addons/dialogmessage.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Simon Poirier -# -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -from ..actions import * - -class DialogMessage(Action): - message = TStringProperty("Message") - position = TArrayProperty((0, 0), 2, 2) - - def __init__(self, **kwargs): - """ - Shows a dialog with a given text, at the given position on the screen. - - Accepted keyword args: - @param message A string to display to the user - @param position A list of the form [x, y] - """ - super(DialogMessage, self).__init__(**kwargs) - self._dialog = None - - def do(self, **kwargs): - """ - Show the dialog - """ - self._dialog = TutoriusDialog(self.message) - self._dialog.set_button_clicked_cb(self._dialog.close_self) - self._dialog.set_modal(False) - self._dialog.move(self.position[0], self.position[1]) - self._dialog.show() - - def undo(self): - """ - Destroy the dialog - """ - if self._dialog: - self._dialog.destroy() - self._dialog = None - -__action__ = { - "name" : "DialogMessage", - "display_name" : "Message Dialog", - "icon" : "window_fullscreen", - "class" : DialogMessage, - "mandatory_props" : ["message"] -} - -# vim:set ts=4 sts=4 sw=4 et: - diff --git a/addons/messagebuttonnext.py b/addons/messagebuttonnext.py index 40e55c2..37d86b4 100644 --- a/addons/messagebuttonnext.py +++ b/addons/messagebuttonnext.py @@ -29,7 +29,7 @@ class MessageButtonNext(EventFilter): MessageButtonNext """ # set message - message = TStringProperty("Message") + message = TStringProperty("Click next to continue") # create the position as an array of fixed-size 2 position = TArrayProperty((0,0), 2, 2) @@ -95,7 +95,7 @@ __event__ = { "display_name" : "Message button next", "icon" : "message-bubble", "class" : MessageButtonNext, - "mandatory_props" : ["message"] + "mandatory_props" : [] } class MsgNext(gtk.EventBox): diff --git a/addons/readfile.py b/addons/readfile.py index 494483c..63fec43 100644 --- a/addons/readfile.py +++ b/addons/readfile.py @@ -47,5 +47,6 @@ __action__ = { "display_name" : "Read File", "icon" : "message-bubble", #FIXME "class" : ReadFile, - "mandatory_props" : ["filename"] + "mandatory_props" : ["filename"], + "test" : True } diff --git a/addons/screenclipper.py b/addons/screenclipper.py new file mode 100644 index 0000000..7ad6808 --- /dev/null +++ b/addons/screenclipper.py @@ -0,0 +1,124 @@ +# 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 +from sugar.tutorius.actions import Action, DragWrapper +from sugar.tutorius.properties import TScreenClipProperty, \ + TArrayProperty +import gtk + +class ScreenClip(Action): + # Create the position as an array of fixed-size 2 + position = TArrayProperty((0,0), 2, 2) + # Do the same for the tail position + clip_image = TScreenClipProperty("") + + def __init__(self, position=None, + clip_image=None, + **kwargs): + """ + Shows a dialog with a given text, at the given position on the screen. + + @param position A list of the form [x, y] + @param clip_image the screen clip image path + @param clip_rect rectangle of the clip image + """ + Action.__init__(self, **kwargs) + + if position: + self.position = position + if clip_image: + self.clip_image = clip_image + + self.overlay = None + self._bubble = None + + def do(self, overlayer=None, **kwargs): + """ + Show the dialog + """ + if overlayer is None: + raise TypeError("Missing overlayer argument") + + self.overlay = overlayer + + if not self._bubble: + # Normal gtk widgets use the parent window to draw themselves. + # For normal layouts, this is good, but as we are using the layout + # for stacking widgets, the rendering order is sometimes wrong. + # Thus, by adding the Image widget in a visible EventBox, we ensure + # the Image is drawn in its own window, and stacking is correct. + x, y = self.position + self._bubble = gtk.EventBox() + self._bubble.set_visible_window(True) + image = gtk.Image() + image.set_from_file(self.clip_image) + self._bubble.add(image) + self._bubble.show_all() + self.overlay.put(self._bubble, x, y) + self.overlay.queue_draw() + + def undo(self): + """ + Destroy the dialog + """ + if self._bubble: + self.overlay.remove(self._bubble) + self._bubble.destroy() + self._bubble = None + + def enter_editmode(self, overlayer=None, *args, **kwargs): + """ + Enters edit mode. The action should display itself in some way, + without affecting the currently running application. + """ + if overlayer is None: + raise TypeError("Missing overlayer argument") + + self.overlay = overlayer + assert not self._drag, "bubble action set to editmode twice" + x, y = self.position + if self.clip_image: + self._bubble = gtk.EventBox() + self._bubble.set_visible_window(True) + image = gtk.Image() + image.set_from_file(self.clip_image) + self._bubble.add(image) + self._bubble.show_all() + self.overlay.put(self._bubble, x, y) + + self._drag = DragWrapper(self._bubble, self.position, + update_action_cb=self.update_property, + draggable=True) + + def exit_editmode(self, *args): + if self._drag: + x, y = self._drag.position + self.position = (int(x), int(y)) + + self._drag.draggable = False + self._drag = None + if self._bubble: + self.overlay.remove(self._bubble) + self._bubble = None + self.overlay = None + +__action__ = { + "name" : ScreenClip.__name__, + "display_name" : "Screen Capture Clip", + "icon" : "screenclip", + "class" : ScreenClip, + "mandatory_props" : [] +} + diff --git a/data/icons/screenclip.svg b/data/icons/screenclip.svg new file mode 100644 index 0000000..67e6680 --- /dev/null +++ b/data/icons/screenclip.svg @@ -0,0 +1,26 @@ + + +]> + + + + + + + + image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/extensions/tutoriusremote.py b/src/extensions/tutoriusremote.py index 129b7b3..2cd449d 100755 --- a/src/extensions/tutoriusremote.py +++ b/src/extensions/tutoriusremote.py @@ -106,6 +106,8 @@ class TPalette(Palette): activity = get_model().get_active_activity() act_name = activity.get_type() + LOGGER.debug("Remote :: Listing tutorial for activity %s", act_name) + tutorial_dict = Vault.list_available_tutorials(act_name) # Build the combo box diff --git a/tests/translatortests.py b/tests/translatortests.py index 05a7831..5b2973b 100644 --- a/tests/translatortests.py +++ b/tests/translatortests.py @@ -129,3 +129,6 @@ class ResourceTranslatorTests(unittest.TestCase): for container in list_action.nested_list: assert getattr(container, "resource").type == "file", "Element of list was not converted properly" + def test_string_translation(self): + # TODO : Once we have enough time. ;) + pass diff --git a/tests/vaulttests.py b/tests/vaulttests.py index 1e39d8c..2fdbc5a 100644 --- a/tests/vaulttests.py +++ b/tests/vaulttests.py @@ -35,7 +35,8 @@ from sugar.tutorius import addon from sugar.tutorius.tutorial import Tutorial from sugar.tutorius.actions import * from sugar.tutorius.filters import * -from sugar.tutorius.vault import Vault, XMLSerializer, Serializer, TutorialBundler +from sugar.tutorius.vault import Vault, XMLSerializer, Serializer, TutorialBundler, \ + LOCALIZATION_FOLDER import sugar @@ -69,6 +70,7 @@ class VaultInterfaceTest(unittest.TestCase): path = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path', 'data', 'tutorius', 'data') if os.path.isdir(path) != True: os.makedirs(path) + os.mkdir(os.path.join(path, LOCALIZATION_FOLDER)) # Generate a first test GUID self.test_guid = uuid1() @@ -407,7 +409,7 @@ class VaultInterfaceTest(unittest.TestCase): bundler = TutorialBundler(self.save_test_guid) - # Add test ressources to the tutorial + # Add test resources to the tutorial test_path = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp') if os.path.isdir(test_path) == True: shutil.rmtree(os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp')) @@ -432,8 +434,14 @@ class VaultInterfaceTest(unittest.TestCase): assert zipfile.is_zipfile(zip_path) # Remove test file os.remove(zip_path) - + def test_get_localization_dir(self): + tutorial = self.fsm + Vault.saveTutorial(tutorial, self.test_metadata_dict) + # Get the localization directory + l10n_dir = Vault.get_localization_dir(self.save_test_guid) + + assert l10n_dir is not None, "Expected valid l10n_dir, got None" def tearDown(self): folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data'); @@ -447,7 +455,6 @@ class VaultInterfaceTest(unittest.TestCase): # Restore home env variable to true value os.environ["HOME"] = self.__old_home - class SerializerInterfaceTest(unittest.TestCase): """ For completeness' sake. 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:/// + 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:/// + 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 +# +# 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 "" + 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 /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): """ -- cgit v0.9.1