Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJCTutorius <charlie@tutorius-dev.(none)>2009-12-27 19:32:14 (GMT)
committer JCTutorius <charlie@tutorius-dev.(none)>2009-12-27 19:32:14 (GMT)
commita1e4d136f860b03abcafc7bf2e2d65b412bc13cd (patch)
tree09089d1ea59fd3b5fdce0b5ab64a842108b6001c
parentc37cd77ae2b3be4cd216d5ffd3130f1e8fb1663c (diff)
parent6d1cf01b2b20a8deabb6bde44777e9823b0201ce (diff)
Merge branch 'master' of gitorious@git.sugarlabs.org:tutorius/mainlineHEADmaster
-rw-r--r--Workshop.activity/Rating.py17
-rw-r--r--Workshop.activity/TutorialStoreCategories.py15
-rw-r--r--Workshop.activity/TutorialStoreDetails.py17
-rw-r--r--Workshop.activity/TutorialStoreHome.py15
-rw-r--r--Workshop.activity/TutorialStoreResults.py19
-rw-r--r--Workshop.activity/TutorialStoreSearch.py17
-rw-r--r--Workshop.activity/TutorialStoreSuggestion.py17
-rwxr-xr-xWorkshop.activity/TutoriusActivity.py7
-rw-r--r--Workshop.activity/Workshop.py12
-rw-r--r--Workshop.activity/WorkshopController.py15
-rw-r--r--Workshop.activity/WorkshopModel.py5
-rw-r--r--addons/bubblemessagewimg.py24
-rw-r--r--addons/dialogmessage.py63
-rw-r--r--addons/messagebuttonnext.py4
-rw-r--r--addons/readfile.py3
-rw-r--r--addons/screenclipper.py124
-rw-r--r--data/icons/screenclip.svg26
-rwxr-xr-xsrc/extensions/tutoriusremote.py2
-rw-r--r--tests/translatortests.py3
-rw-r--r--tests/vaulttests.py15
-rw-r--r--tutorius/TProbe.py245
-rw-r--r--tutorius/addon.py13
-rw-r--r--tutorius/creator.py62
-rw-r--r--tutorius/engine.py17
-rw-r--r--tutorius/localization.py68
-rw-r--r--tutorius/properties.py27
-rw-r--r--tutorius/propwidgets.py186
-rw-r--r--tutorius/service.py8
-rw-r--r--tutorius/translator.py96
-rw-r--r--tutorius/vault.py90
30 files changed, 1066 insertions, 166 deletions
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 <simpoir@gmail.com>
-#
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-
-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 @@
+<?xml version="1.0" ?><!-- Created with Inkscape (http://www.inkscape.org/) --><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#000000">
+ <!ENTITY fill_color "#ffffff">
+]><svg height="51" id="svg2" inkscape:version="0.47 r22583" sodipodi:docname="New document 1" version="1.1" width="51" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg">
+ <defs id="defs4">
+ <inkscape:perspective id="perspective10" inkscape:persp3d-origin="372.04724 : 350.78739 : 1" inkscape:vp_x="0 : 526.18109 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="744.09448 : 526.18109 : 1" sodipodi:type="inkscape:persp3d"/>
+ </defs>
+ <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="213.0625" inkscape:cy="36.59674" inkscape:document-units="px" inkscape:guide-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="628" inkscape:window-maximized="0" inkscape:window-width="1093" inkscape:window-x="20" inkscape:window-y="20" inkscape:zoom="0.96" pagecolor="#ffffff" showgrid="false" showguides="true"/>
+ <metadata id="metadata7">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1" transform="translate(0,-1001.3622)">
+ <rect height="37.728832" id="rect2816" style="color:#000000;fill:none;stroke:&stroke_color;;stroke-width:3.72792578;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:7.45585251, 7.45585251;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" width="32.628826" x="12.63153" y="1006.964"/>
+ <g id="g3609" transform="matrix(0,-1.2919257,-1.2002032,0,1263.209,1094.1684)">
+ <path d="m 38.46875,29.0625 2.125,3.625 c 0,0 -4.09196,3.6458 -4,6.4375 0.09196,2.7917 2.387558,7.1156 5.53125,6.9375 3.640064,-1.181 0.537287,-9.13245 1.0625,-12.40625 1.990315,3.0125 5.834605,10.3756 9.75,9.5 1.098405,-0.9127 2.29467,-4.6001 0.34375,-7.6875 C 51.255431,32.26285 45.84375,32 45.84375,32 L 46.5625,13.90625 43.34375,11.5 l -1.1875,19.4375 -3.6875,-8.1875 0,6.3125 z" id="path3607" style="color:#000000;fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.9485935;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" transform="translate(0,1001.3622)"/>
+ <path d="m 49.54564,1037.9255 c 0.369232,0.5291 0.738464,1.0581 1.107696,1.5872" id="path3592" style="color:#000000;fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:2.288059;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/>
+ <path d="m 40.618302,1040.3023 c 0.01806,0.1307 -0.09311,0.2591 -0.07504,0.3898 0.02955,0.2138 0.214643,0.01 0.04284,0.3101 -0.02316,0.041 -0.10904,0.034 -0.117883,0.08 -0.01769,0.092 0.178412,0.1384 0.160722,0.2303 -0.0088,0.046 -0.09472,0.039 -0.117884,0.08 -0.03276,0.058 -0.02501,0.1299 -0.03752,0.1949 -0.01251,0.065 -0.04655,0.1295 -0.03752,0.1949 0.0064,0.046 0.05357,0.077 0.08036,0.1151 0.01428,0.1034 0.04174,0.2057 0.04284,0.3101 0.004,0.3766 -0.266515,0.1154 -0.07505,0.3898" id="path3594" style="color:#000000;fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.9485935;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/>
+ </g>
+ </g>
+</svg> \ 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://<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):
"""