Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCharlie <charlie@tutorius-dev.(none)>2009-10-25 01:18:10 (GMT)
committer Charlie <charlie@tutorius-dev.(none)>2009-10-25 01:18:10 (GMT)
commit48ba2db44bb2054222cad3aca3e027e24c51c090 (patch)
tree13a25552db428a3a97e22e09f7936c4a988f11cf
parent2b4eaea8f46e846799595b3c7015f77c6156532b (diff)
parentaa4868af13437f4718e8ce7972b79b496d296068 (diff)
Merge branch 'demo' into activity
-rw-r--r--Workshop.activity/MANIFEST5
-rw-r--r--Workshop.activity/TutorialStoreCategories.py28
-rw-r--r--Workshop.activity/TutorialStoreDetails.py74
-rw-r--r--Workshop.activity/TutorialStoreHome.py109
-rw-r--r--Workshop.activity/TutorialStoreResults.py124
-rw-r--r--Workshop.activity/TutorialStoreSearch.py33
-rw-r--r--Workshop.activity/TutorialStoreSuggestion.py139
-rwxr-xr-xWorkshop.activity/TutoriusActivity.py77
-rw-r--r--Workshop.activity/Workshop.py285
-rw-r--r--Workshop.activity/WorkshopController.py119
-rw-r--r--Workshop.activity/WorkshopListItem.py79
-rw-r--r--Workshop.activity/WorkshopModel.py162
-rw-r--r--Workshop.activity/activity/activity.info8
-rw-r--r--Workshop.activity/activity/someicon.svg21
-rw-r--r--Workshop.activity/arrow_back.pngbin0 -> 310 bytes
-rw-r--r--Workshop.activity/dialogs.py139
-rw-r--r--Workshop.activity/full_star.pngbin0 -> 1031 bytes
-rw-r--r--Workshop.activity/grayed_star.pngbin0 -> 930 bytes
-rw-r--r--Workshop.activity/half_star.pngbin0 -> 890 bytes
-rw-r--r--Workshop.activity/icon.svg21
-rwxr-xr-xWorkshop.activity/setup.py3
-rw-r--r--addons/bubblemessage.py25
-rw-r--r--addons/chainaction.py44
-rw-r--r--addons/clickaction.py52
-rw-r--r--addons/dialogmessage.py8
-rw-r--r--addons/disablewidget.py59
-rw-r--r--addons/gtkwidgeteventfilter.py14
-rw-r--r--addons/gtkwidgettypefilter.py100
-rw-r--r--addons/oncewrapper.py59
-rw-r--r--addons/readfile.py56
-rw-r--r--addons/timerevent.py73
-rw-r--r--addons/triggereventfilter.py46
-rw-r--r--addons/typetextaction.py57
-rw-r--r--addons/widgetidentifyaction.py47
-rw-r--r--data/icons/chain.svg65
-rw-r--r--data/icons/clock.svg269
-rw-r--r--data/icons/once_wrapper.svg74
-rw-r--r--setup.cfg.example (renamed from setup.cfg)0
-rwxr-xr-xsetup.py1
-rw-r--r--tests/actiontests.py169
-rw-r--r--tests/addontests.py50
-rw-r--r--tests/bundlertests.py65
-rw-r--r--tests/constraintstests.py15
-rw-r--r--tests/coretests.py429
-rw-r--r--tests/filterstests.py22
-rw-r--r--tests/linear_creatortests.py10
-rw-r--r--tests/probetests.py63
-rw-r--r--tests/propertiestests.py192
-rwxr-xr-xtests/run-tests.py74
-rw-r--r--tests/serializertests.py197
-rw-r--r--tests/storetests.py107
-rw-r--r--tests/vaulttests.py516
-rw-r--r--tutorius/TProbe.py506
-rw-r--r--tutorius/actions.py153
-rw-r--r--tutorius/addon.py21
-rw-r--r--tutorius/bundler.py558
-rw-r--r--tutorius/constraints.py5
-rw-r--r--tutorius/core.py234
-rw-r--r--tutorius/creator.py630
-rw-r--r--tutorius/dbustools.py41
-rw-r--r--tutorius/engine.py48
-rw-r--r--tutorius/filters.py132
-rw-r--r--tutorius/linear_creator.py3
-rw-r--r--tutorius/overlayer.py14
-rw-r--r--tutorius/properties.py71
-rw-r--r--tutorius/service.py85
-rw-r--r--tutorius/services.py3
-rw-r--r--tutorius/store.py173
-rw-r--r--tutorius/uam/__init__.py3
-rw-r--r--tutorius/ui/creator.glade209
-rw-r--r--tutorius/vault.py861
-rw-r--r--tutorius/viewer.py406
72 files changed, 6848 insertions, 1692 deletions
diff --git a/Workshop.activity/MANIFEST b/Workshop.activity/MANIFEST
new file mode 100644
index 0000000..c5c6d42
--- /dev/null
+++ b/Workshop.activity/MANIFEST
@@ -0,0 +1,5 @@
+TutoriusActivity.py
+activity/someicon.svg
+activity/activity.info
+setup.py
+MANIFEST
diff --git a/Workshop.activity/TutorialStoreCategories.py b/Workshop.activity/TutorialStoreCategories.py
new file mode 100644
index 0000000..2bf843d
--- /dev/null
+++ b/Workshop.activity/TutorialStoreCategories.py
@@ -0,0 +1,28 @@
+import sys, os
+import gtk
+
+class TutorialStoreCategories:
+
+ def __init__(self):
+ categorie_math = gtk.Label('Math (8)')
+ categorie_physics = gtk.Label('Phyisics (16)')
+ categorie_history = gtk.Label('History (32)')
+ categorie_learning = gtk.Label('Learning (53)')
+
+ categorie_box = gtk.VBox(True, 5)
+ self.categorie_box_frame = gtk.Frame('Categories')
+ self.categorie_box_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ categorie_box.pack_start(categorie_math, True, True, 4)
+ categorie_box.pack_start(categorie_physics, True, True, 4)
+ categorie_box.pack_start(categorie_history, True, True, 4)
+ categorie_box.pack_start(categorie_learning, True, True, 4)
+
+ self.categorie_box_frame.add(categorie_box)
+
+ categorie_math.show()
+ categorie_physics.show()
+ categorie_history.show()
+ categorie_learning.show()
+ categorie_box.show()
+ self.categorie_box_frame.show() \ No newline at end of file
diff --git a/Workshop.activity/TutorialStoreDetails.py b/Workshop.activity/TutorialStoreDetails.py
new file mode 100644
index 0000000..67b85d0
--- /dev/null
+++ b/Workshop.activity/TutorialStoreDetails.py
@@ -0,0 +1,74 @@
+import sys, os
+import gtk
+
+class TutorialStoreDetails:
+
+ def __init__(self):
+ tuto_icon = gtk.Image()
+ tuto_icon.set_from_file('icon.svg')
+
+ full_star_icon1 = gtk.Image()
+ full_star_icon1.set_from_file('full_star.svg')
+ full_star_icon2 = gtk.Image()
+ full_star_icon2.set_from_file('full_star.svg')
+ full_star_icon3 = gtk.Image()
+ full_star_icon3.set_from_file('full_star.svg')
+
+ grayed_star_icon = gtk.Image()
+ grayed_star_icon.set_from_file('grayed_star.svg')
+
+ half_star_icon= gtk.Image()
+ half_star_icon.set_from_file('half_star.svg')
+
+ title_autor_box = gtk.VBox()
+ tuto_title_label = gtk.Label('<Title>')
+ tuto_author_label = gtk.Label('by <Author>')
+ title_autor_box.pack_start(tuto_title_label)
+ title_autor_box.pack_start(tuto_author_label)
+
+ tuto_descrip_label = gtk.Label('Description 1 : Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.')
+ tuto_descrip_label.set_line_wrap(True)
+ tuto_descrip_label.set_width_chars(50)
+
+ download_button = gtk.Button('Download')
+ infos_button = gtk.Button('Informations')
+ comp_button = gtk.Button('Compatibility')
+
+ tutorial_title_bar = gtk.HBox (False, 4)
+ tutorial_title_bar.pack_start(tuto_icon)
+ tutorial_title_bar.pack_start(title_autor_box)
+ tutorial_title_bar.pack_start(full_star_icon1)
+ tutorial_title_bar.pack_start(full_star_icon2)
+ tutorial_title_bar.pack_start(full_star_icon3)
+ tutorial_title_bar.pack_start(half_star_icon)
+ tutorial_title_bar.pack_start(grayed_star_icon)
+ tutorial_button_bar = gtk.HBox(True, 25)
+ tutorial_button_bar.pack_start(download_button)
+ tutorial_button_bar.pack_start(infos_button)
+ tutorial_button_bar.pack_start(comp_button)
+
+ self.tutorial_store_details = gtk.VBox (False, 5)
+ back_button = gtk.Button('Back')
+
+ self.tutorial_store_details.pack_start(back_button)
+ self.tutorial_store_details.pack_start(tutorial_title_bar)
+ self.tutorial_store_details.pack_start(tuto_descrip_label)
+ self.tutorial_store_details.pack_start(tutorial_button_bar)
+
+
+ tuto_icon.show()
+ full_star_icon1.show()
+ full_star_icon2.show()
+ full_star_icon3.show()
+ grayed_star_icon.show()
+ half_star_icon.show()
+ tuto_title_label.show()
+ tuto_author_label.show()
+ title_autor_box.show()
+ tuto_descrip_label.show()
+ tutorial_title_bar.show()
+ tutorial_button_bar.show()
+ download_button.show()
+ infos_button.show()
+ comp_button.show()
+ self.tutorial_store_details.show()
diff --git a/Workshop.activity/TutorialStoreHome.py b/Workshop.activity/TutorialStoreHome.py
new file mode 100644
index 0000000..8a361dc
--- /dev/null
+++ b/Workshop.activity/TutorialStoreHome.py
@@ -0,0 +1,109 @@
+import logging
+import TutorialStoreCategories
+import TutorialStoreSearch
+import TutorialStoreSuggestion
+import TutorialStoreResults
+import TutorialStoreDetails
+
+import sys, os
+import gtk
+
+class TutorialStoreHome:
+ def log(self,widget,data=None):
+ logging.info('Tutorial Store Home start')
+
+ def __init__(self):
+
+ self.categories = TutorialStoreCategories.TutorialStoreCategories()
+ categories_frame = self.categories.categorie_box_frame
+
+ self.search = TutorialStoreSearch.TutorialStoreSearch()
+ tutorial_store_search = self.search.tutorial_store_search
+
+ self.search_button = self.search.search_button_access()
+
+ self.suggestion = TutorialStoreSuggestion.TutorialStoreSuggestion()
+
+ tut_store_suggestion = gtk.HBox(homogeneous=True, spacing=5)
+ tut_store_suggestion.pack_start(self.suggestion.top_five_frame, expand=False, fill=False, padding=0)
+ tut_store_suggestion.pack_start(self.suggestion.also_like_frame, expand=False, fill=False, padding=0)
+
+ self.results = TutorialStoreResults.TutorialStoreResults()
+
+ tut_store_home_base = gtk.VBox(False, 5)
+ tut_store_home_base.pack_start(tutorial_store_search, False, False, 25)
+ tut_store_home_base.pack_start(tut_store_suggestion, False, False, 0)
+
+ self.labeltest = gtk.Label('Test')
+
+ self.tutorial_store_home = gtk.HBox(False, 5)
+ self.tutorial_store_home.pack_start(categories_frame, True, True, 5)
+ self.tutorial_store_home.pack_start(tut_store_home_base, True, True, 5)
+ self.tutorial_store_home.pack_start(tut_store_home_base, True, True, 5)
+
+ tut_store_suggestion.show()
+ categories_frame.show()
+ tut_store_home_base.show()
+ self.tutorial_store_home.show()
+
+ def get_search_button(self):
+
+ return self.search_button
+
+ def get_more_button(self):
+ return self.suggestion.get_more_button()
+
+ def get_results_widget(self):
+
+ self.search = TutorialStoreSearch.TutorialStoreSearch()
+ tutorial_store_search = self.search.tutorial_store_search
+
+ self.results = TutorialStoreResults.TutorialStoreResults()
+ tutorial_store_results = self.results.tutorial_store_results
+
+ self.categories = TutorialStoreCategories.TutorialStoreCategories()
+ categories_frame = self.categories.categorie_box_frame
+
+ tut_store_home_base = gtk.VBox(False, 5)
+ tut_store_home_base.pack_start(tutorial_store_search, False, False, 25)
+ tut_store_home_base.pack_start(tutorial_store_results, False, False, 0)
+
+ self.tutorial_store_home = gtk.HBox(False, 5)
+ self.tutorial_store_home.pack_start(categories_frame, True, True, 5)
+ self.tutorial_store_home.pack_start(tut_store_home_base, True, True, 5)
+
+ tut_store_home_base.show()
+ tutorial_store_search.show()
+ tutorial_store_results.show()
+ categories_frame.show()
+ self.tutorial_store_home.show()
+
+ return self.tutorial_store_home
+
+ def get_details_widget(self):
+
+ self.search = TutorialStoreSearch.TutorialStoreSearch()
+ tutorial_store_search = self.search.tutorial_store_search
+
+ self.details = TutorialStoreDetails.TutorialStoreDetails()
+ tutorial_store_details = self.details.tutorial_store_details
+
+ self.categories = TutorialStoreCategories.TutorialStoreCategories()
+ categories_frame = self.categories.categorie_box_frame
+
+ tut_store_home_base = gtk.VBox(False, 5)
+ tut_store_home_base.pack_start(tutorial_store_search, False, False, 25)
+ tut_store_home_base.pack_start(tutorial_store_details, False, False, 0)
+
+ self.tutorial_store_home = gtk.HBox(False, 5)
+ self.tutorial_store_home.pack_start(categories_frame, True, True, 5)
+ self.tutorial_store_home.pack_start(tut_store_home_base, True, True, 5)
+
+ tut_store_home_base.show()
+ tutorial_store_search.show()
+ tutorial_store_details.show()
+ categories_frame.show()
+ self.tutorial_store_home.show()
+
+ return self.tutorial_store_home
+
diff --git a/Workshop.activity/TutorialStoreResults.py b/Workshop.activity/TutorialStoreResults.py
new file mode 100644
index 0000000..0e8e54c
--- /dev/null
+++ b/Workshop.activity/TutorialStoreResults.py
@@ -0,0 +1,124 @@
+import sys, os
+import gtk
+
+class TutorialStoreResults:
+
+ def __init__(self):
+ tuto_icon1 = gtk.Image()
+ tuto_icon1.set_from_file('icon.svg')
+ tuto_icon2 = gtk.Image()
+ tuto_icon2.set_from_file('icon.svg')
+
+ full_star_icon1 = gtk.Image()
+ full_star_icon1.set_from_file('full_star.svg')
+ full_star_icon2 = gtk.Image()
+ full_star_icon2.set_from_file('full_star.svg')
+ full_star_icon3 = gtk.Image()
+ full_star_icon3.set_from_file('full_star.svg')
+ full_star_icon4 = gtk.Image()
+ full_star_icon4.set_from_file('full_star.svg')
+ full_star_icon5 = gtk.Image()
+ full_star_icon5.set_from_file('full_star.svg')
+
+ grayed_star_icon1 = gtk.Image()
+ grayed_star_icon1.set_from_file('grayed_star.svg')
+ grayed_star_icon2 = gtk.Image()
+ grayed_star_icon2.set_from_file('grayed_star.svg')
+ grayed_star_icon3 = gtk.Image()
+ grayed_star_icon3.set_from_file('grayed_star.svg')
+ grayed_star_icon4 = gtk.Image()
+ grayed_star_icon4.set_from_file('grayed_star.svg')
+
+ half_star_icon1= gtk.Image()
+ half_star_icon1.set_from_file('half_star.svg')
+
+ tuto_title_label1 = gtk.Label('Titre 1')
+ tuto_title_label2 = gtk.Label('Titre 2')
+
+ tuto_descrip_label1 = gtk.Label('Description 1 : Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.')
+ tuto_descrip_label1.set_line_wrap(True)
+ tuto_descrip_label1.set_width_chars(50)
+ tuto_descrip_label2 = gtk.Label('Description 2 : Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.')
+ tuto_descrip_label2.set_line_wrap(True)
+ tuto_descrip_label2.set_width_chars(50)
+
+ download_button1 = gtk.Button('Download')
+ download_button2 = gtk.Button('Download')
+ details_button1 = gtk.Button('Details')
+ details_button2 = gtk.Button('Details')
+
+ show_resuls_label = gtk.Label('Showing results 1-29 of 109')
+ # TODO : Probably must be something else than a label, or find a way to listen to click event on label ...
+ next_results_label = gtk.Label('< 1 2 3 4 >')
+
+ tutorial_title_bar1 = gtk.HBox (False, 0)
+ tutorial_title_bar1.pack_start(tuto_icon1)
+ tutorial_title_bar1.pack_start(tuto_title_label1)
+ tutorial_title_bar1.pack_start(full_star_icon1)
+ tutorial_title_bar1.pack_start(full_star_icon2)
+ tutorial_title_bar1.pack_start(full_star_icon3)
+ tutorial_title_bar1.pack_start(grayed_star_icon1)
+ tutorial_title_bar1.pack_start(grayed_star_icon2)
+
+ tutorial_button_bar1 = gtk.HBox(True, 25)
+ tutorial_button_bar1.pack_start(download_button1)
+ tutorial_button_bar1.pack_start(details_button1)
+
+ tutorial1 = gtk.VBox (False, 5)
+ tutorial1.pack_start(tutorial_title_bar1)
+ tutorial1.pack_start(tuto_descrip_label1)
+ tutorial1.pack_start(tutorial_button_bar1)
+
+ tutorial_title_bar2 = gtk.HBox (False, 5)
+ tutorial_title_bar2.pack_start(tuto_icon2)
+ tutorial_title_bar2.pack_start(tuto_title_label2)
+ tutorial_title_bar2.pack_start(full_star_icon4)
+ tutorial_title_bar2.pack_start(full_star_icon5)
+ tutorial_title_bar2.pack_start(half_star_icon1)
+ tutorial_title_bar2.pack_start(grayed_star_icon3)
+ tutorial_title_bar2.pack_start(grayed_star_icon4)
+
+ tutorial_button_bar2 = gtk.HBox(True, 55)
+ tutorial_button_bar2.pack_start(download_button2)
+ tutorial_button_bar2.pack_start(details_button2)
+
+ tutorial2 = gtk.VBox (False, 5)
+ tutorial2.pack_start(tutorial_title_bar2)
+ tutorial2.pack_start(tuto_descrip_label2)
+ tutorial2.pack_start(tutorial_button_bar2)
+
+ self.tutorial_store_results = gtk.VBox(False, 10)
+ self.tutorial_store_results.pack_start(tutorial1, True, True, 10)
+ self.tutorial_store_results.pack_start(tutorial2, True, True, 10)
+ self.tutorial_store_results.pack_start(show_resuls_label)
+ self.tutorial_store_results.pack_start(next_results_label)
+
+ tuto_icon1.show()
+ tuto_icon2.show()
+ full_star_icon1.show()
+ full_star_icon2.show()
+ full_star_icon3.show()
+ full_star_icon4.show()
+ full_star_icon5.show()
+ grayed_star_icon1.show()
+ grayed_star_icon2.show()
+ grayed_star_icon3.show()
+ grayed_star_icon4.show()
+ half_star_icon1.show()
+ tuto_title_label1.show()
+ tuto_title_label2.show()
+ tuto_descrip_label1.show()
+ tuto_descrip_label2.show()
+ download_button1.show()
+ download_button2.show()
+ details_button1.show()
+ details_button2.show()
+ show_resuls_label.show()
+ next_results_label.show()
+ tutorial_title_bar1.show()
+ tutorial_title_bar2.show()
+ tutorial_button_bar1.show()
+ tutorial_button_bar2.show()
+ tutorial1.show()
+ tutorial2.show()
+ self.tutorial_store_results.show() \ No newline at end of file
diff --git a/Workshop.activity/TutorialStoreSearch.py b/Workshop.activity/TutorialStoreSearch.py
new file mode 100644
index 0000000..1663f80
--- /dev/null
+++ b/Workshop.activity/TutorialStoreSearch.py
@@ -0,0 +1,33 @@
+import sys, os
+import gtk
+
+class TutorialStoreSearch:
+
+ def __init__(self):
+ search_label = gtk.Label('Search :')
+ search_box = gtk.Entry(400)
+ in_label = gtk.Label('in')
+ search_combobox = gtk.combo_box_new_text()
+ search_combobox.insert_text(0, 'all Categories (109)')
+ search_combobox.insert_text(1, 'Math (8)')
+ search_combobox.insert_text(2, 'Physics (16)')
+ search_combobox.insert_text(3, 'History (32)')
+ search_combobox.insert_text(4, 'Learning (53)')
+ self.search_button = gtk.Button('Search')
+
+ self.tutorial_store_search = gtk.HBox(False, 5)
+ self.tutorial_store_search.pack_start(search_label, True, True, 5)
+ self.tutorial_store_search.pack_start(search_box, True, True, 5)
+ self.tutorial_store_search.pack_start(in_label, True, True, 5)
+ self.tutorial_store_search.pack_start(search_combobox, True, True, 5)
+ self.tutorial_store_search.pack_start(self.search_button, True, True, 5)
+
+ search_label.show()
+ search_box.show()
+ in_label.show()
+ search_combobox.show()
+ self.search_button.show()
+ self.tutorial_store_search.show()
+
+ def search_button_access(self):
+ return self.search_button \ No newline at end of file
diff --git a/Workshop.activity/TutorialStoreSuggestion.py b/Workshop.activity/TutorialStoreSuggestion.py
new file mode 100644
index 0000000..9164ca0
--- /dev/null
+++ b/Workshop.activity/TutorialStoreSuggestion.py
@@ -0,0 +1,139 @@
+import sys, os
+import gtk
+
+class TutorialStoreSuggestion:
+
+ def __init__(self):
+
+ tutorial1 = gtk.HBox(homogeneous=True, spacing=0)
+ tutorial2 = gtk.HBox(homogeneous=True, spacing=0)
+ tutorial3 = gtk.HBox(homogeneous=True, spacing=0)
+ tutorial4 = gtk.HBox(homogeneous=True, spacing=0)
+ tutorial5 = gtk.HBox(homogeneous=True, spacing=0)
+ tutorial6 = gtk.HBox(homogeneous=True, spacing=0)
+
+ icon1 = gtk.Image()
+ icon1.set_from_file('icon.svg')
+ icon2 = gtk.Image()
+ icon2.set_from_file('icon.svg')
+ icon3 = gtk.Image()
+ icon3.set_from_file('icon.svg')
+ icon4 = gtk.Image()
+ icon4.set_from_file('icon.svg')
+ icon5 = gtk.Image()
+ icon5.set_from_file('icon.svg')
+ icon6 = gtk.Image()
+ icon6.set_from_file('icon.svg')
+
+ label1 = gtk.Label('Tuto 1')
+ label2 = gtk.Label('Tuto 2')
+ label3 = gtk.Label('Tuto 3')
+
+ label4 = gtk.Label('Tuto 4')
+ label5 = gtk.Label('Tuto 5')
+ label6 = gtk.Label('Tuto 6')
+
+ self.more_button1 = gtk.Button('More')
+ more_button2 = gtk.Button('More')
+ more_button3 = gtk.Button('More')
+ more_button4 = gtk.Button('More')
+ more_button5 = gtk.Button('More')
+ more_button6 = gtk.Button('More')
+
+ tutorial1.pack_start(icon1, expand=True, fill=True, padding=4)
+ tutorial1.pack_start(label1, expand=True, fill=True, padding=0)
+ tutorial1.pack_start(self.more_button1, expand=False, fill=False, padding=5)
+ tutorial1_frame = gtk.Frame()
+ tutorial1_frame.add(tutorial1)
+ tutorial1_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ tutorial2.pack_start(icon2, expand=True, fill=True, padding=4)
+ tutorial2.pack_start(label2, expand=True, fill=True, padding=0)
+ tutorial2.pack_start(more_button2, expand=False, fill=False, padding=5)
+ tutorial2_frame = gtk.Frame()
+ tutorial2_frame.add(tutorial2)
+ tutorial2_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ tutorial3.pack_start(icon3, expand=True, fill=True, padding=4)
+ tutorial3.pack_start(label3, expand=True, fill=True, padding=0)
+ tutorial3.pack_start(more_button3, expand=False, fill=False, padding=5)
+ tutorial3_frame = gtk.Frame()
+ tutorial3_frame.add(tutorial3)
+ tutorial3_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ tutorial4.pack_start(icon4, expand=True, fill=True, padding=4)
+ tutorial4.pack_start(label4, expand=True, fill=True, padding=0)
+ tutorial4.pack_start(more_button4, expand=False, fill=False, padding=5)
+ tutorial4_frame = gtk.Frame()
+ tutorial4_frame.add(tutorial4)
+ tutorial4_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ tutorial5.pack_start(icon5, expand=True, fill=True, padding=4)
+ tutorial5.pack_start(label5, expand=True, fill=True, padding=0)
+ tutorial5.pack_start(more_button5, expand=False, fill=False, padding=5)
+ tutorial5_frame = gtk.Frame()
+ tutorial5_frame.add(tutorial5)
+ tutorial5_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ tutorial6.pack_start(icon6, expand=True, fill=True, padding=4)
+ tutorial6.pack_start(label6, expand=True, fill=True, padding=0)
+ tutorial6.pack_start(more_button6, expand=False, fill=False, padding=5)
+ tutorial6_frame = gtk.Frame()
+ tutorial6_frame.add(tutorial6)
+ tutorial6_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ top_five = gtk.VBox(homogeneous=True, spacing=0)
+ self.top_five_frame = gtk.Frame('Top 5 Most Popular')
+## top_five_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0))
+
+ top_five.pack_start(tutorial1_frame, expand=True, fill=True, padding=0)
+ top_five.pack_start(tutorial2_frame, expand=True, fill=True, padding=0)
+ top_five.pack_start(tutorial3_frame, expand=True, fill=True, padding=0)
+
+ also_like = gtk.VBox(homogeneous=True, spacing=0)
+ self.also_like_frame = gtk.Frame('You might also like :')
+
+ also_like.pack_start(tutorial4_frame, expand=True, fill=True, padding=0)
+ also_like.pack_start(tutorial5_frame, expand=True, fill=True, padding=0)
+ also_like.pack_start(tutorial6_frame, expand=True, fill=True, padding=0)
+
+ self.top_five_frame.add(top_five)
+ self.also_like_frame.add(also_like)
+
+ label1.show()
+ label2.show()
+ label3.show()
+ label4.show()
+ label5.show()
+ label6.show()
+ icon1.show()
+ icon2.show()
+ icon3.show()
+ icon4.show()
+ icon5.show()
+ icon6.show()
+ self.more_button1.show()
+ more_button2.show()
+ more_button3.show()
+ more_button4.show()
+ more_button5.show()
+ more_button6.show()
+ tutorial1_frame.show()
+ tutorial2_frame.show()
+ tutorial3_frame.show()
+ tutorial4_frame.show()
+ tutorial5_frame.show()
+ tutorial6_frame.show()
+ tutorial1.show()
+ tutorial2.show()
+ tutorial3.show()
+ tutorial4.show()
+ tutorial5.show()
+ tutorial6.show()
+ top_five.show()
+ self.top_five_frame.show()
+ also_like.show()
+ self.also_like_frame.show()
+
+ def get_more_button(self):
+ return self.more_button1 \ No newline at end of file
diff --git a/Workshop.activity/TutoriusActivity.py b/Workshop.activity/TutoriusActivity.py
new file mode 100755
index 0000000..b262a9f
--- /dev/null
+++ b/Workshop.activity/TutoriusActivity.py
@@ -0,0 +1,77 @@
+from sugar.activity import activity
+import TutorialStoreHome
+from Workshop import WorkshopView
+import logging
+
+import sys, os
+import gtk
+from dialogs import LoginDialog
+
+class TutoriusActivity(activity.Activity):
+ def hello(self,widget,data=None):
+ logging.info('Hello world')
+
+ def callback(self, widget, button_string):
+
+ if button_string == 'search_button':
+ self.right_container.remove(self.tutorial_store_home.tutorial_store_home)
+
+ results_widget = self.tutorial_store_home.get_results_widget()
+
+ self.right_container.pack_start(results_widget)
+
+ results_widget.show()
+ self.right_container.show()
+
+ elif button_string == 'more_button':
+ self.right_container.remove(self.tutorial_store_home.tutorial_store_home)
+
+ details_widget = self.tutorial_store_home.get_details_widget()
+
+ self.right_container.pack_start(details_widget)
+
+ details_widget.show()
+ self.right_container.show()
+
+ def __init__(self,handle):
+ print "running activity init", handle
+ activity.Activity.__init__(self,handle)
+ print "actiity running"
+
+ toolbox = activity.ActivityToolbox(self)
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ self.table = gtk.HPaned()
+ self.table.set_position(100)
+ self.left_container = gtk.VBox()
+ btn1 = gtk.Button("My tutorials")
+ btn2 = gtk.Button("Tutorial Store")
+ btn3 = gtk.Button("test button")
+
+ self.left_container.pack_start(btn1,expand=False)
+ self.left_container.pack_start(btn2,expand=False)
+ self.tutorial_store_home = TutorialStoreHome.TutorialStoreHome()
+
+ tutorial_store_search_button = self.tutorial_store_home.get_search_button()
+ tutorial_store_search_button.connect("clicked", self.callback, 'search_button')
+
+ tutorial_store_more_button = self.tutorial_store_home.get_more_button()
+ tutorial_store_more_button.connect("clicked", self.callback, 'more_button')
+
+ self.right_container = gtk.VBox()
+ #self.right_container.pack_start(self.tutorial_store_home.tutorial_store_home)
+
+ self.workshop = WorkshopView()
+
+ self.table.add1(self.left_container)
+ self.table.add2(self.workshop)
+ self.set_canvas(self.table)
+ btn3.show()
+ btn1.show()
+ btn2.show()
+ self.left_container.show()
+ self.workshop.show()
+ self.table.show()
+
+ print "AT THE END OF THE CLASS"
diff --git a/Workshop.activity/Workshop.py b/Workshop.activity/Workshop.py
new file mode 100644
index 0000000..1ca6be2
--- /dev/null
+++ b/Workshop.activity/Workshop.py
@@ -0,0 +1,285 @@
+import gtk
+
+from WorkshopListItem import WorkshopListItem,Rating
+from WorkshopModel import WorkshopModel
+from WorkshopController import WorkshopController
+
+class WorkshopView(gtk.Alignment):
+ def __init__(self):
+ gtk.Alignment.__init__(self,0.0,0.0,1.0,1.0)
+ self.model = WorkshopModel(self)
+ self.controller = WorkshopController(self,self.model)
+
+ self.mainView = WorkshopMain(self.controller)
+ self.detailView = WorkshopDetail(None,self.controller)
+
+ self.add(self.mainView)
+
+ self.mainView.show()
+ self.detailView.show()
+
+ self.controller.tutorial_query(None,None)
+
+ def set_tutorial_list(self,tutorial_list):
+ """
+ Set the list of tutorial to display in the main View
+ Refresh the View
+
+ @param tutorial_list the list of tutorial
+ """
+ self.mainView.set_tutorial_list(tutorial_list)
+
+
+ def change_sorting(self,sorting_key):
+ """
+ Sort the list of tutorial base on the sorting_key
+
+ @param sorting_key the tutorial metadata to use to sort the tutorials
+ """
+ self.mainView.change_sorting(sorting_key)
+
+ def refresh_tutorial_info(self):
+ """
+ Tell the view to refresh the content of the tutorial list because some info have changed
+
+ Call this function when information about tutorial have changed, but the tutorial to display are the same
+ """
+ pass
+
+ def toggle_tutorial_publish(self,tutorial,published):
+ """
+ Change the publish/unpublish status of the tutorial on the view
+
+ @param tutorial the tutorial to change the status
+ @param published True if the tutorial is published, False if not
+ """
+ pass
+
+ def display_detail(self,tutorial):
+ """
+ Displays the detail view of a tutorial
+
+ @param tutorial the tutorial to display
+ """
+ self.mainView.hide()
+ self.remove(self.mainView)
+ print tutorial.name
+ self.detailView = WorkshopDetail(tutorial,self.controller)
+ self.add(self.detailView)
+ self.detailView.show()
+
+ def display_main_view(self):
+ """
+ Displays the main view of the Workshop
+ """
+ self.detailView.hide()
+ self.remove(self.detailView)
+ self.add(self.mainView)
+ self.mainView.show()
+
+ def display_info_dialog(self,tutorial):
+ """
+ Displays the infos dialog on a tutorial
+
+ @param tutorial the tutorial to edit
+ """
+ pass
+
+class WorkshopMain(gtk.VBox):
+ def __init__(self,controller):
+ gtk.VBox.__init__(self,False,10)
+
+ self.controller = controller
+
+ self.set_border_width(10)
+ self.search_bar = SearchBar(self.controller)
+ self.pack_start(self.search_bar,False,False)
+
+ sep = gtk.HSeparator()
+ self.pack_start(sep,False,False)
+ self.main_container = gtk.ScrolledWindow()
+ self.main_container.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)
+
+ self.list_container= gtk.VBox()
+
+ self.main_container.add_with_viewport(self.list_container)
+ self.pack_start(self.main_container)
+
+ self.search_bar.show()
+ self.list_container.show()
+ self.main_container.show()
+ sep.show()
+ self.tutorial_list = []
+ self.sorting_key = 'Name'
+
+
+ def change_sorting(self,sorting):
+ self.sorting_key = sorting
+ self.sort_tutorial()
+
+ def sort_tutorial(self):
+ self.tutorial_list.sort(lambda x, y:
+ cmp(str(getattr(x,self.sorting_key.lower())).lower(),str(getattr(y,self.sorting_key.lower())).lower()))
+ self.refresh_tutorial_display()
+
+ def set_tutorial_list(self,tutorial_list):
+ self.tutorial_list = tutorial_list
+ self.sort_tutorial()
+
+ def refresh_tutorial_display(self):
+ for child in self.list_container.get_children():
+ self.list_container.remove(child)
+ for tuto in self.tutorial_list:
+ item = WorkshopListItem(tuto,self.controller)
+ self.list_container.pack_start(item)
+ item.show()
+
+class SearchBar(gtk.HBox):
+ def __init__(self,controller):
+ gtk.HBox.__init__(self,False,10)
+ self.set_border_width(5)
+ self.controller = controller
+
+ self.search_entry = gtk.Entry(400)
+ self.search_button = gtk.Button("Go")
+ self.separator = gtk.VSeparator()
+ self.sort_label = gtk.Label("Sort by")
+ self.sort_combo = gtk.combo_box_new_text()
+ self.sort_combo.insert_text(0,"Name")
+ self.sort_combo.insert_text(1,"Rating")
+ self.sort_combo.set_active(0)
+ self.selected_sorting = self.sort_combo.get_active_text()
+
+ self.pack_start(self.search_entry,padding=5)
+ self.pack_start(self.search_button,False,False,padding=10)
+ self.pack_start(self.separator,False,False,padding=10)
+ self.pack_start(self.sort_label,False,False,padding=5)
+ self.pack_start(self.sort_combo,)
+
+ self.search_entry.show()
+ self.search_button.show()
+ self.separator.show()
+ self.sort_label.show()
+ self.sort_combo.show()
+
+ self.search_button.connect("clicked",self.controller.tutorial_query,self.search_entry.get_text())
+ self.sort_combo.connect("changed",self.controller.sort_selection_changed,None)
+
+ def get_sorting(self):
+ return self.selected_sorting
+
+ sorting = property(get_sorting)
+
+
+
+class WorkshopDetail(gtk.VBox):
+ def __init__(self,tutorial,controller):
+ if tutorial is None:
+ return
+
+ self.title_text = '<span size="xx-large">%(title)s</span>'
+ self.author_text = '<span size="large">by %(author)s</span>'
+ self.desc_text = 'Description: %(description)s'
+ self.controller = controller
+ gtk.VBox.__init__(self,False,10)
+ self.set_border_width(10)
+
+ first_row = gtk.HBox(False)
+ back_image = gtk.Image()
+ back_image.set_from_file('arrow_back.png')
+ self.back_button = gtk.Button("Back")
+ self.back_button.set_image(back_image)
+
+ first_row.pack_start(self.back_button,False,False)
+
+ second_row = gtk.HBox(False)
+ icon = gtk.Image()
+ icon.set_from_file('icon.svg')
+
+ label_holder = gtk.VBox(False,10)
+
+ self.title_label = gtk.Label(tutorial.name)
+ self.title_label.set_alignment(0.0,0.5)
+ self.author_label = gtk.Label(tutorial.author)
+ self.author_label.set_alignment(0.05,0.5)
+
+ label_holder.pack_start(self.title_label)
+ label_holder.pack_start(self.author_label)
+
+ self.rating = Rating(tutorial.rating)
+
+ second_row.pack_start(icon,False,False)
+ second_row.pack_start(label_holder)
+ second_row.pack_end(self.rating,False,False)
+
+ self.desc_view = gtk.TextView()
+ self.desc_buff = gtk.TextBuffer()
+ self.desc_buff.set_text(tutorial.description)
+ self.desc_view.set_buffer(self.desc_buff)
+ self.desc_view.set_editable(False)
+ self.desc_view.set_wrap_mode(gtk.WRAP_WORD)
+ self.desc_view.set_cursor_visible(False)
+ self.desc_view.connect("realize",self.realize_cb,None)
+ self.desc_view.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("gray") )
+
+ fourth_row = gtk.HBox(False,15)
+ self.launch_button = gtk.Button('Launch')
+ self.launch_button.get_child().set_markup(self.title_text %{"title":"Launch"})
+ self.edit_button = gtk.Button('Edit')
+ self.edit_button.get_child().set_markup(self.title_text %{"title":"Edit"})
+ self.update_button = gtk.Button('Update')
+ self.update_button.get_child().set_markup(self.title_text %{"title":"Update"})
+ self.info_button = gtk.Button('Infos')
+ self.info_button.get_child().set_markup(self.title_text %{"title":"Infos"})
+ self.delete_button = gtk.Button('Delete')
+ self.delete_button.get_child().set_markup(self.title_text %{"title":"Delete"})
+
+ fourth_row.pack_start(self.launch_button,False,False)
+ fourth_row.pack_start(self.edit_button,False,False)
+ fourth_row.pack_start(self.update_button,False,False)
+ fourth_row.pack_start(self.info_button,False,False)
+ fourth_row.pack_end(self.delete_button,False,False)
+
+ fifth_row = gtk.HBox(False,15)
+ self.publish_button = gtk.Button('')
+ self.publish_button.get_child().set_markup(self.title_text %{"title":"Publish"})
+ self.unpublish_button = gtk.Button('')
+ self.unpublish_button.get_child().set_markup(self.title_text %{"title":"Unpublish"})
+
+ fifth_row.pack_start(self.publish_button,False,False)
+ fifth_row.pack_start(self.unpublish_button,False,False)
+
+ self.pack_start(first_row,False,False)
+ self.pack_start(second_row,False,False)
+ self.pack_start(self.desc_view)
+ self.pack_end(fifth_row,False,False)
+ self.pack_end(fourth_row,False,False)
+
+
+ self.back_button.show()
+ first_row.show()
+ self.title_label.show()
+ self.author_label.show()
+ self.rating.show()
+ label_holder.show()
+ second_row.show()
+ icon.show()
+ self.desc_view.show()
+ self.launch_button.show()
+ self.edit_button.show()
+ self.update_button.show()
+ self.info_button.show()
+ self.delete_button.show()
+ fourth_row.show()
+
+ self.publish_button.show()
+ self.unpublish_button.show()
+ fifth_row.show()
+
+ self.title_label.set_markup(self.title_text % {"title":tutorial.name})
+ self.author_label.set_markup(self.author_text % {"author":tutorial.author})
+
+ self.back_button.connect("clicked",self.controller.back_pressed,None)
+
+ def realize_cb(self,widget,data=None):
+ widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)) \ No newline at end of file
diff --git a/Workshop.activity/WorkshopController.py b/Workshop.activity/WorkshopController.py
new file mode 100644
index 0000000..ba192cf
--- /dev/null
+++ b/Workshop.activity/WorkshopController.py
@@ -0,0 +1,119 @@
+"""
+WorkshopController
+
+This module handles user action from the workshop view
+
+"""
+import logging
+from WorkshopModel import Tutorial
+class WorkshopController():
+ def __init__(self,view,model):
+ self.view = view
+ self.model = model
+
+ def tutorial_query(self,widget,keyword):
+ """
+ Handles query from the view
+
+ @param widget the widget that sent the query
+ @param keyword the keyword for the query, empty to get all tutorials
+ """
+ self.model.query(keyword)
+
+ def sort_selection_changed(self,widget,data):
+ """
+ Handles selection changes in the sorting selection
+
+ @param widget the widget that sent the query
+ @param sort the property to use to sort tutorial
+ """
+ sorting = widget.get_active_text()
+ self.view.change_sorting(sorting)
+
+
+ def launch_tutorial_triggered(self,widget,tutorial):
+ """
+ Handles start tutorial action
+
+ @param widget the widget that triggered the action
+ @param tutorial the tutorial to launch
+ """
+ pass
+
+ def show_details(self,widget,tutorial):
+ """
+ show the details for a tutorial
+
+ @param widget the widget that made the call
+ @param tutorial the tutorial to
+ """
+ self.view.display_detail(tutorial)
+
+ def back_pressed(self,widget,data):
+ """
+ When in detail view, go back to general view
+
+ @param widget the widget that made the call
+ @param data not used
+ """
+ self.view.display_main_view()
+
+ def rate_tutorial(self,widget,data):
+ """
+ Change the rating for a tutorial
+
+ @param widget the widget that made the call
+ @param data a tuple (tutorial,new_rating)
+ """
+ pass
+
+ def edit_tutorial(self,widget,tutorial):
+ """
+ Edit the tutorial in the detail view
+
+ @param widget the widget that made the call
+ @param tutorial the tutorial to edit
+ """
+ pass
+
+ def update_tutorial(self,widget,tutorial):
+ """
+ Need to know what this do
+ """
+ pass
+
+ def info_tutorial(self,widget,tutorial):
+ """
+ Edit the infos about the tutorial
+
+ @param widget the widget that made the call
+ @param tutorial the tutorial to edit
+ """
+ pass
+
+ def delete_tutorial(self,widget,tutorial):
+ """
+ Delete a tutorial
+
+ @param widget the widget that made the call
+ @param tutorial the tutorial to delete
+ """
+ self.model.delete_tutorial(tutorial)
+
+ def publish_tutorial(self,widget,tutorial):
+ """
+ Publish a tutorial
+
+ @param widget the widget that made the call
+ @param tutorial the tutorial to publish
+ """
+ self.model.publish_tutorial(tutorial)
+
+ def unpublish_tutorial(self,widget,tutorial):
+ """
+ Unpublish a tutorial
+
+ @param widget the widget that made the call
+ @param tutorial the tutorial to unpublish
+ """
+ self.model.unpublish_tutorial(tutorial)
diff --git a/Workshop.activity/WorkshopListItem.py b/Workshop.activity/WorkshopListItem.py
new file mode 100644
index 0000000..8b9fe53
--- /dev/null
+++ b/Workshop.activity/WorkshopListItem.py
@@ -0,0 +1,79 @@
+import gtk
+
+class WorkshopListItem(gtk.Alignment):
+ def __init__(self,tutorial,controller):
+ gtk.Alignment.__init__(self,0.0,0.0,1.0,1.0)
+ self.tutorial = tutorial
+ self.controller = controller
+
+ self.title_text = '<span size="xx-large">%(title)s</span>'
+ self.table = gtk.Table(3,3,False)
+
+ self.lbl_title = gtk.Label('')
+ self.lbl_desc = gtk.Label(tutorial.description)
+ self.lbl_desc.set_line_wrap(True)
+ self.btn_launch = gtk.Button('Launch')
+ launch_align= gtk.Alignment(0.0,1.0,0.0,0.0)
+ launch_align.add(self.btn_launch)
+ self.btn_details = gtk.Button('Details')
+ self.icon = gtk.Image()
+ self.icon.set_from_file('icon.svg')
+ self.rating = Rating(tutorial.rating)
+
+ self.table.attach(self.icon,0,1,0,1,0,0)
+ self.table.attach(self.lbl_title,1,2,0,1,yoptions=0)
+ self.table.attach(self.lbl_desc,1,2,1,2,xoptions=gtk.FILL,yoptions=gtk.EXPAND)
+ self.table.attach(launch_align,1,2,2,3,gtk.FILL)
+ self.table.attach(self.btn_details,2,3,2,3,0,0)
+ self.table.attach(self.rating,2,3,0,2,0,0)
+
+ self.table.set_row_spacing(1,10)
+
+ self.lbl_title.set_alignment(0.0,0.5)
+ self.lbl_title.set_markup(self.title_text % {'title':tutorial.name})
+ self.lbl_desc.set_alignment(0.0,0.5)
+
+ self.table.show()
+ self.icon.show()
+ self.lbl_title.show()
+ launch_align.show()
+ self.lbl_desc.show()
+ self.btn_launch.show()
+ self.btn_details.show()
+ self.rating.show()
+
+ self.add(self.table)
+
+ self.btn_details.connect("clicked",self.controller.show_details,self.tutorial)
+
+class Rating(gtk.HBox):
+ def __init__(self,rating):
+ gtk.HBox.__init__(self,False,4)
+ value = rating
+ self.stars = [0,0,0,0,0]
+ if value > 0:
+ for x in range(5):
+ if value -1 > 0:
+ self.stars[x]=1
+ elif value -1 == -0.5:
+ self.stars[x] = 0.5
+ break
+ else:
+ self.stars[x]=1
+ break
+ value -= 1
+ self.prepare_image()
+
+ def prepare_image(self):
+ for x in self.stars:
+ if x == 0:
+ filename='grayed_star.png'
+ elif x == 0.5:
+ filename='half_star.png'
+ elif x == 1:
+ filename='full_star.png'
+ image = gtk.Image()
+ image.set_from_file(filename)
+ self.pack_start(image)
+ image.show()
+
diff --git a/Workshop.activity/WorkshopModel.py b/Workshop.activity/WorkshopModel.py
new file mode 100644
index 0000000..159263d
--- /dev/null
+++ b/Workshop.activity/WorkshopModel.py
@@ -0,0 +1,162 @@
+"""
+WorkshopModel
+
+This module is the model of the Workshop Activity
+"""
+
+from sugar.tutorius.vault import *
+
+class WorkshopModel():
+ def __init__(self,view):
+ self.view = view
+
+ def query(self,keyword):
+ """
+ Query the vault for tutorial that are linked with the keyword
+ Update the currently managed tutorials and notifies the view that the managed tutorials have changed
+
+ @param keyword the keyword for the query
+ """
+## t1 = Tutorial({'name':'tuto 1',"description":"This is the description","rating":5})
+## t2 = Tutorial({'name':'tuto 2',"description":"This is the description of another","rating":4})
+## t3 = Tutorial({'name':'tuto 3',"description":"This is the description oh the last","rating":3})
+## t4 = Tutorial({'name':'tuto 4',"description":"This is the description oh the last","rating":1})
+## t5 = Tutorial({'name':'tuto 5',"description":"This is the description oh the last","rating":1})
+## t6 = Tutorial({'name':'tuto 6',"description":"This is the description oh the last","rating":1})
+## t7 = Tutorial({'name':'tuto 7',"description":"This is the description oh the last","rating":1})
+## tutorial_list = [t1,t2,t3,t4,t5,t6,t7]
+
+ vault_return = Vault.query()
+ tutorial_list = []
+ for tuto in vault_return:
+ tutorial_list.append(Tutorial(tuto))
+
+ self.view.set_tutorial_list(tutorial_list)
+
+ def delete_tutorial(self,tutorial):
+ """
+ Delete a tutorial and updated the currently managed tutorials
+ Notifies the view that the manages tutorials have changed
+
+ @param tutorial the tutorial to delete
+ """
+ pass
+
+ def update_tutorial_infos(self,tutorial, new_infos):
+ """
+ Updates the metadata on a tutorial and updates the currently managed tutorials
+ Notifies the view tha the managed tutorials have changed
+
+ @param tutorial the tutorial to update
+ @param new_infos a dictionnary of the new informations i.e. {"title":"tut1","author":"Max Power"}
+ """
+ pass
+
+ def publish_tutorial(self,tutorial):
+ """
+ Publishes a tutorial
+
+ Details to come
+ """
+ pass
+
+ def unpublish_tutorial(self,tutorial):
+ """
+ Unpublishes a tutorial
+
+ Details to come
+ """
+ pass
+
+
+class Tutorial():
+ """
+ Wrapper for tutorial metadata
+ """
+ def __init__(self,metadata_dict):
+ self.__original_dict = metadata_dict
+ self.__update_dict = metadata_dict
+
+ if 'name' in self.__original_dict:
+ self.__name = self.__original_dict['name']
+ else:
+ self.__description = name
+
+ if 'description' in self.__original_dict:
+ self.__description = self.__original_dict['description']
+ else:
+ self.__description = ""
+
+ if 'author' in self.__original_dict:
+ self.__author = self.__original_dict['author']
+ else:
+ self.__author = ""
+
+ if 'rating' in self.__original_dict:
+ self.__rating = float(self.__original_dict['rating'])
+ else:
+ self.__rating = 0
+
+ if 'publish_state' in self.__original_dict:
+ self.__published_state = self.__original_dict['publish_state']
+ else:
+ self.__published_state = None
+
+ if 'guid' in self.__original_dict:
+ self.__id = self.__original_dict['guid']
+ else:
+ self.__id = ""
+
+ def get_name(self):
+ return self.__name
+
+ def set_name(self,name):
+ self.__name = name
+
+ def get_description(self):
+ return self.__description
+
+ def set_description(self,description):
+ self.__description = description
+ self.__update_dict['Description'] = description
+
+ def get_author(self):
+ return self.__author
+
+ def set_author(self,author):
+ self.__author = author
+ self.__update_dict['Author'] = author
+
+ def get_rating(self):
+ return self.__rating
+
+ def set_rating(self,rating):
+ self.__rating = rating
+ self.__update_dict['Rating'] = rating
+
+ def get_published_state(self):
+ return self.__published_state
+
+ def set_published_state(self,published_state):
+ self.__published_state = published_state
+ self.__update_dict['PublishedState'] = published_state
+
+ def get_id(self):
+ return self.__id
+
+ def set_id(self,id):
+ self.__id = id
+ self.__update_dict['TutorialId'] = id
+
+ def get_updated_metadata(self):
+ return self.__update_dict
+
+ name = property(get_name,set_name)
+ description = property(get_description,set_description)
+ author = property(get_author,set_author)
+ rating = property(get_rating,set_rating)
+ published_state = property(get_published_state,set_published_state)
+ id = property(get_id,set_id)
+ updated_metadata = property(get_updated_metadata)
+
+
diff --git a/Workshop.activity/activity/activity.info b/Workshop.activity/activity/activity.info
new file mode 100644
index 0000000..02dbee1
--- /dev/null
+++ b/Workshop.activity/activity/activity.info
@@ -0,0 +1,8 @@
+[Activity]
+name = Tutorius
+bundle_id = org.laptop.TutoriusActivity
+class = TutoriusActivity.TutoriusActivity
+icon = someicon
+activity_version = 1
+host_version = 1
+show_launcher = yes
diff --git a/Workshop.activity/activity/someicon.svg b/Workshop.activity/activity/someicon.svg
new file mode 100644
index 0000000..bb28f04
--- /dev/null
+++ b/Workshop.activity/activity/someicon.svg
@@ -0,0 +1,21 @@
+<?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="55px" id="svg2393" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.47pre1 r21720" sodipodi:docname="tutortool.svg" sodipodi:version="0.32" version="1.1" width="55px" 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="defs2395">
+ <inkscape:perspective id="perspective2401" inkscape:persp3d-origin="16 : 10.666667 : 1" inkscape:vp_x="0 : 16 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="32 : 16 : 1" sodipodi:type="inkscape:persp3d"/>
+ </defs>
+ <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="3.7661233" inkscape:cy="33.132055" inkscape:document-units="px" inkscape:grid-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="675" inkscape:window-maximized="0" inkscape:window-width="1057" inkscape:window-x="108" inkscape:window-y="45" inkscape:zoom="3.9590209" pagecolor="#ffffff" showgrid="true"/>
+ <metadata id="metadata2398">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1" transform="translate(0,23)">
+ <path d="m 38.01548,1.5478747 c 0,7.1837999 -7.3667,13.0141283 -16.443525,13.0141283 -2.269208,0 -8.124729,3.152936 -13.9608513,4.252763 C 13.382177,14.110994 11.434521,11.926642 9.9463815,10.748864 6.9701032,8.3933076 5.1284282,5.1397735 5.1284282,1.5478747 c 0,-7.1837994 7.3666998,-13.0141297 16.4435268,-13.0141297 9.076825,0 16.443525,5.8303303 16.443525,13.0141297 z" id="path2403" sodipodi:nodetypes="cscsssc" style="fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.96931934;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/>
+ <path d="m 50.150276,6.4721386 c 0,2.621116 -1.428036,4.9953144 -3.735846,6.7142344 -1.153905,0.85946 -1.824287,2.434433 1.398853,6.784273 -6.258422,-3.991066 -8.65379,-4.001712 -10.413335,-4.001712 -7.03818,0 -12.750327,-4.254565 -12.750327,-9.4967954 0,-5.2422321 5.712147,-9.4967971 12.750327,-9.4967971 7.038182,0 12.750328,4.254565 12.750328,9.4967971 z" id="path3175" sodipodi:nodetypes="cccsssc" style="fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.96931934;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/Workshop.activity/arrow_back.png b/Workshop.activity/arrow_back.png
new file mode 100644
index 0000000..8263674
--- /dev/null
+++ b/Workshop.activity/arrow_back.png
Binary files differ
diff --git a/Workshop.activity/dialogs.py b/Workshop.activity/dialogs.py
new file mode 100644
index 0000000..1cf135d
--- /dev/null
+++ b/Workshop.activity/dialogs.py
@@ -0,0 +1,139 @@
+import gtk
+
+class LoginDialog(gtk.Dialog):
+ def __init__(self):
+ gtk.Dialog.__init__(self,"Login",None,gtk.DIALOG_MODAL)
+ ok_button = gtk.Button("Login")
+ ok_button.connect("clicked",self.ok_clicked,None)
+ ok_button.show()
+ self.action_area.pack_start(ok_button)
+ self.add_button("Cancel",gtk.RESPONSE_REJECT)
+ self.set_resizable(False)
+ self.prepare_dialog()
+
+
+ def prepare_dialog(self):
+ self.user_line = gtk.HBox(False,10)
+ self.pass_line = gtk.HBox(False,10)
+ self.register_line = gtk.HBox(False)
+ self.remember_line = gtk.HBox(False)
+
+ self.username_lbl = gtk.Label("Username:")
+ self.username_entry = gtk.Entry()
+ self.username_entry.set_width_chars(40)
+
+ self.password_lbl = gtk.Label("Password:")
+ self.password_entry = gtk.Entry()
+ self.password_entry.set_visibility(False)
+ self.password_entry.set_width_chars(40)
+
+ self.register_me = gtk.LinkButton("","Register Now!")
+ self.not_registered = gtk.Label("Not Registered? ")
+
+ self.remember_user = gtk.CheckButton("Remember my username",False)
+
+ self.register_me.connect("clicked",self.click_link,None)
+
+ self.user_line.pack_start(self.username_lbl,False)
+ self.user_line.pack_end(self.username_entry,False)
+
+ self.pass_line.pack_start(self.password_lbl,False)
+ self.pass_line.pack_end(self.password_entry,False)
+
+ self.register_line.pack_end(self.register_me,False)
+ self.register_line.pack_end(self.not_registered,False)
+
+ self.remember_line.pack_start(self.remember_user,False,padding=80)
+
+ self.vbox.pack_start(self.register_line,False)
+ self.vbox.pack_start(self.user_line,False,padding=10)
+ self.vbox.pack_start(self.pass_line,False,padding=10)
+ self.vbox.pack_start(self.remember_line,False)
+
+ self.vbox.show()
+ self.user_line.show()
+ self.pass_line.show()
+ self.register_line.show()
+ self.remember_line.show()
+
+ self.username_lbl.show()
+ self.username_entry.show()
+
+ self.password_lbl.show()
+ self.password_entry.show()
+
+ self.register_me.show()
+ self.not_registered.show()
+
+ self.remember_user.show()
+
+ def ok_clicked(self,widget,data=None):
+ self.response(gtk.RESPONSE_ACCEPT)
+
+ def click_link(self,widget,data=None):
+ self.register_dialog = RegisterDialog()
+ self.register_dialog.run()
+ self.register_dialog.destroy()
+
+class RegisterDialog(gtk.Dialog):
+ def __init__(self):
+ gtk.Dialog.__init__(self,"Register",None,gtk.DIALOG_MODAL)
+ ok_button = gtk.Button("Register")
+ ok_button.connect("clicked",self.ok_clicked,None)
+ ok_button.show()
+ self.action_area.pack_start(ok_button)
+ self.add_button("Cancel",gtk.RESPONSE_REJECT)
+ self.set_resizable(False)
+ self.create_content()
+
+ def ok_clicked(self,widget,data=None):
+ self.response(gtk.RESPONSE_ACCEPT)
+
+ def create_content(self):
+ entry_length = 40
+ table = gtk.Table(10,4,False)
+ labels = ["Email address","Name","Username","Password","Confirmation","Location","Web Site"]
+ required=[True,True,True,True,True,False,False]
+ hidden=[False,False,False,True,True,False,False]
+ for x in range(6):
+ lbl = gtk.Label(labels[x]+":")
+ entry = gtk.Entry()
+ entry.set_width_chars(entry_length)
+ table.attach(lbl,0,1,x,x+1,xpadding=10)
+ table.attach(entry,1,2,x,x+1,ypadding=10)
+ if hidden[x]:
+ entry.set_visibility(False)
+ if required[x]:
+ required_lbl = gtk.Label("*")
+ table.attach(required_lbl,2,3,x,x+1)
+ required_lbl.show()
+ lbl.set_alignment(0.0,0.5)
+ lbl.show()
+ entry.show()
+
+
+ if_required = gtk.Label(" * Required Field")
+ table.attach(if_required,3,4,0,1)
+
+ under_13 = gtk.CheckButton("I am 13 years old or younger",False)
+ legal = gtk.CheckButton("I have read the",False)
+ legal_notices=gtk.LinkButton('',"legal notices")
+ and_lbl = gtk.Label("and")
+ privacy = gtk.LinkButton('',"privacy statement")
+ hbox = gtk.HBox(False,0)
+ hbox.pack_start(legal)
+ hbox.pack_start(legal_notices)
+ hbox.pack_start(and_lbl)
+ hbox.pack_start(privacy)
+
+ table.attach(under_13,1,2,7,8)
+ table.attach(hbox,1,2,8,9)
+ under_13.show()
+ legal.show()
+ legal_notices.show()
+ and_lbl.show()
+ privacy.show()
+ hbox.show()
+ if_required.show()
+ table.show()
+ self.vbox.pack_start(table)
diff --git a/Workshop.activity/full_star.png b/Workshop.activity/full_star.png
new file mode 100644
index 0000000..9f7d095
--- /dev/null
+++ b/Workshop.activity/full_star.png
Binary files differ
diff --git a/Workshop.activity/grayed_star.png b/Workshop.activity/grayed_star.png
new file mode 100644
index 0000000..7f8b1e1
--- /dev/null
+++ b/Workshop.activity/grayed_star.png
Binary files differ
diff --git a/Workshop.activity/half_star.png b/Workshop.activity/half_star.png
new file mode 100644
index 0000000..48ebae3
--- /dev/null
+++ b/Workshop.activity/half_star.png
Binary files differ
diff --git a/Workshop.activity/icon.svg b/Workshop.activity/icon.svg
new file mode 100644
index 0000000..bb28f04
--- /dev/null
+++ b/Workshop.activity/icon.svg
@@ -0,0 +1,21 @@
+<?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="55px" id="svg2393" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.47pre1 r21720" sodipodi:docname="tutortool.svg" sodipodi:version="0.32" version="1.1" width="55px" 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="defs2395">
+ <inkscape:perspective id="perspective2401" inkscape:persp3d-origin="16 : 10.666667 : 1" inkscape:vp_x="0 : 16 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="32 : 16 : 1" sodipodi:type="inkscape:persp3d"/>
+ </defs>
+ <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="3.7661233" inkscape:cy="33.132055" inkscape:document-units="px" inkscape:grid-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="675" inkscape:window-maximized="0" inkscape:window-width="1057" inkscape:window-x="108" inkscape:window-y="45" inkscape:zoom="3.9590209" pagecolor="#ffffff" showgrid="true"/>
+ <metadata id="metadata2398">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1" transform="translate(0,23)">
+ <path d="m 38.01548,1.5478747 c 0,7.1837999 -7.3667,13.0141283 -16.443525,13.0141283 -2.269208,0 -8.124729,3.152936 -13.9608513,4.252763 C 13.382177,14.110994 11.434521,11.926642 9.9463815,10.748864 6.9701032,8.3933076 5.1284282,5.1397735 5.1284282,1.5478747 c 0,-7.1837994 7.3666998,-13.0141297 16.4435268,-13.0141297 9.076825,0 16.443525,5.8303303 16.443525,13.0141297 z" id="path2403" sodipodi:nodetypes="cscsssc" style="fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.96931934;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/>
+ <path d="m 50.150276,6.4721386 c 0,2.621116 -1.428036,4.9953144 -3.735846,6.7142344 -1.153905,0.85946 -1.824287,2.434433 1.398853,6.784273 -6.258422,-3.991066 -8.65379,-4.001712 -10.413335,-4.001712 -7.03818,0 -12.750327,-4.254565 -12.750327,-9.4967954 0,-5.2422321 5.712147,-9.4967971 12.750327,-9.4967971 7.038182,0 12.750328,4.254565 12.750328,9.4967971 z" id="path3175" sodipodi:nodetypes="cccsssc" style="fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.96931934;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/Workshop.activity/setup.py b/Workshop.activity/setup.py
new file mode 100755
index 0000000..f95b630
--- /dev/null
+++ b/Workshop.activity/setup.py
@@ -0,0 +1,3 @@
+#! /usr/bin/env python
+from sugar.activity import bundlebuilder
+bundlebuilder.start()
diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py
index a859ef8..2bd2d31 100644
--- a/addons/bubblemessage.py
+++ b/addons/bubblemessage.py
@@ -13,31 +13,34 @@
# 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 *
+from sugar.tutorius.actions import Action, DragWrapper
+from sugar.tutorius.properties import TStringProperty, TArrayProperty
+from sugar.tutorius import overlayer
+from sugar.tutorius.services import ObjectStore
class BubbleMessage(Action):
message = TStringProperty("Message")
# Create the position as an array of fixed-size 2
- position = TArrayProperty([0,0], 2, 2)
+ position = TArrayProperty((0,0), 2, 2)
# Do the same for the tail position
- tail_pos = TArrayProperty([0,0], 2, 2)
+ tail_pos = TArrayProperty((0,0), 2, 2)
- def __init__(self, message=None, pos=None, speaker=None, tailpos=None):
+ def __init__(self, message=None, position=None, speaker=None, tail_pos=None):
"""
Shows a dialog with a given text, at the given position on the screen.
@param message A string to display to the user
- @param pos A list of the form [x, y]
+ @param position A list of the form [x, y]
@param speaker treeish representation of the speaking widget
- @param tailpos The position of the tail of the bubble; useful to point to
+ @param tail_pos The position of the tail of the bubble; useful to point to
specific elements of the interface
"""
Action.__init__(self)
- if pos:
- self.position = pos
- if tailpos:
- self.tail_pos = tailpos
+ if position:
+ self.position = position
+ if tail_pos:
+ self.tail_pos = tail_pos
if message:
self.message = message
@@ -94,7 +97,7 @@ class BubbleMessage(Action):
def exit_editmode(self, *args):
x,y = self._drag.position
- self.position = [int(x), int(y)]
+ self.position = (int(x), int(y))
if self._drag:
self._drag.draggable = False
self._drag = None
diff --git a/addons/chainaction.py b/addons/chainaction.py
new file mode 100644
index 0000000..43c4fa4
--- /dev/null
+++ b/addons/chainaction.py
@@ -0,0 +1,44 @@
+# 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 *
+
+class ChainAction(Action):
+ actions = TAddonListProperty()
+
+ """Utility class to allow executing actions in a specific order"""
+ def __init__(self, actions=[]):
+ """ChainAction(action1, ... ) builds a chain of actions"""
+ Action.__init__(self)
+ self.actions = actions
+
+ def do(self,**kwargs):
+ """do() each action in the chain"""
+ for act in self.actions:
+ act.do(**kwargs)
+
+ def undo(self):
+ """undo() each action in the chain, starting with the last"""
+ for act in reversed(self.actions):
+ act.undo()
+
+__action__ = {
+ 'name': 'ChainAction',
+ 'display_name' : 'Chain of actions',
+ 'icon' : 'chain',
+ 'class' : ChainAction,
+ 'mandatory_props' : ['actions']
+}
diff --git a/addons/clickaction.py b/addons/clickaction.py
new file mode 100644
index 0000000..828dd75
--- /dev/null
+++ b/addons/clickaction.py
@@ -0,0 +1,52 @@
+# 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 import gtkutils
+from sugar.tutorius.actions import *
+
+class ClickAction(Action):
+ """
+ Action that simulate a click on a widget
+ Work on any widget that implements a clicked() method
+
+ @param widget The threehish representation of the widget
+ """
+ widget = TStringProperty("")
+ def __init__(self, widget):
+ Action.__init__(self)
+ self.widget = widget
+
+ def do(self):
+ """
+ click the widget
+ """
+ realWidget = gtkutils.find_widget(ObjectStore().activity, self.widget)
+ if hasattr(realWidget, "clicked"):
+ realWidget.clicked()
+
+ def undo(self):
+ """
+ No undo
+ """
+ pass
+
+__action__ = {
+ 'name' : 'ClickAction',
+ 'display_name' : 'Click',
+ 'icon' : 'format-justify-center',
+ 'class' : ClickAction,
+ 'mandatory_props' : ['widget']
+}
diff --git a/addons/dialogmessage.py b/addons/dialogmessage.py
index 22a223b..f15f256 100644
--- a/addons/dialogmessage.py
+++ b/addons/dialogmessage.py
@@ -20,21 +20,21 @@ from sugar.tutorius.actions import *
class DialogMessage(Action):
message = TStringProperty("Message")
- position = TArrayProperty([0, 0], 2, 2)
+ position = TArrayProperty((0, 0), 2, 2)
- def __init__(self, message=None, pos=None):
+ def __init__(self, message=None, position=None):
"""
Shows a dialog with a given text, at the given position on the screen.
@param message A string to display to the user
- @param pos A list of the form [x, y]
+ @param position A list of the form [x, y]
"""
super(DialogMessage, self).__init__()
self._dialog = None
if message:
self.message = message
- if pos: self.position = pos
+ if position: self.position = position
def do(self):
"""
diff --git a/addons/disablewidget.py b/addons/disablewidget.py
new file mode 100644
index 0000000..ce3f235
--- /dev/null
+++ b/addons/disablewidget.py
@@ -0,0 +1,59 @@
+# 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 *
+from sugar.tutorius import gtkutils
+from sugar.tutorius.services import ObjectStore
+
+class DisableWidgetAction(Action):
+ target = TStringProperty("0")
+
+ def __init__(self, target):
+ """Constructor
+ @param target target treeish
+ """
+ Action.__init__(self)
+ if target is not None:
+ self.target = target
+ self._widget = None
+
+ def do(self):
+ """Action do"""
+ os = ObjectStore()
+ if os.activity:
+ self._widget = gtkutils.find_widget(os.activity, self.target)
+ if self._widget:
+ # If we have an object whose sensitivity we can query, we will
+ # keep it to reset it in the undo() method
+ if hasattr(self._widget, 'get_sensitive') and callable(self._widget.get_sensitive):
+ self._previous_sensitivity = self._widget.get_sensitive()
+ self._widget.set_sensitive(False)
+
+ def undo(self):
+ """Action undo"""
+ if self._widget:
+ if hasattr(self, '_previous_sensitivity'):
+ self._widget.set_sensitive(self._previous_sensitivity)
+ else:
+ self._widget.set_sensitive(True)
+
+__action__ = {
+ 'name' : 'DisableWidgetAction',
+ 'display_name' : 'Disable Widget',
+ 'icon' : 'stop',
+ 'class' : DisableWidgetAction,
+ 'mandatory_props' : ['target']
+}
diff --git a/addons/gtkwidgeteventfilter.py b/addons/gtkwidgeteventfilter.py
index cbfb00c..5811744 100644
--- a/addons/gtkwidgeteventfilter.py
+++ b/addons/gtkwidgeteventfilter.py
@@ -13,23 +13,23 @@
# 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.filters import *
-from sugar.tutorius.properties import *
+from sugar.tutorius.filters import EventFilter
+from sugar.tutorius.properties import TUAMProperty, TGtkSignal
+from sugar.tutorius.gtkutils import find_widget
class GtkWidgetEventFilter(EventFilter):
"""
Basic Event filter for Gtk widget events
"""
object_id = TUAMProperty()
- event_name = TStringProperty("clicked")
+ event_name = TGtkSignal('clicked')
- def __init__(self, next_state=None, object_id=None, event_name=None):
+ def __init__(self, object_id=None, event_name=None):
"""Constructor
- @param next_state default EventFilter param, passed on to EventFilter
@param object_id object fqdn-style identifier
@param event_name event to attach to
"""
- super(GtkWidgetEventFilter,self).__init__(next_state)
+ super(GtkWidgetEventFilter,self).__init__()
self._callback = None
self.object_id = object_id
self.event_name = event_name
@@ -64,6 +64,6 @@ __event__ = {
"display_name" : "GTK Event catcher",
"icon" : "player_play",
"class" : GtkWidgetEventFilter,
- "mandatory_props" : ["object_id"]
+ "mandatory_props" : ["object_id", "event_name"]
}
diff --git a/addons/gtkwidgettypefilter.py b/addons/gtkwidgettypefilter.py
new file mode 100644
index 0000000..816a754
--- /dev/null
+++ b/addons/gtkwidgettypefilter.py
@@ -0,0 +1,100 @@
+# 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.filters import *
+from sugar.tutorius.properties import *
+from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.gtkutils import find_widget
+
+import logging
+logger = logging.getLogger("GtkWidgetTypeFilter")
+
+class GtkWidgetTypeFilter(EventFilter):
+ """
+ Event Filter that listens for keystrokes on a widget
+ """
+ object_id = TStringProperty("")
+ text = TStringProperty("")
+ strokes = TArrayProperty([])
+
+ def __init__(self, object_id, text=None, strokes=None):
+ """Constructor
+ @param next_state default EventFilter param, passed on to EventFilter
+ @param object_id object tree-ish identifier
+ @param text resulting text expected
+ @param strokes list of strokes expected
+
+ At least one of text or strokes must be supplied
+ """
+ super(GtkWidgetTypeFilter, self).__init__()
+ self.object_id = object_id
+ self.text = text
+ self._captext = ""
+ self.strokes = strokes
+ self._capstrokes = []
+ self._widget = None
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install handlers
+ @param callback default EventFilter callback arg
+ """
+ super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs)
+ logger.debug("~~~GtkWidgetTypeFilter install")
+ activity = ObjectStore().activity
+ if activity is None:
+ logger.error("No activity")
+ raise RuntimeWarning("no activity in the objectstore")
+
+ self._widget = find_widget(activity, self.object_id)
+ if self._widget:
+ self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb)
+ logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self.object_id) )
+
+ def remove_handlers(self):
+ """remove handlers"""
+ super(GtkWidgetTypeFilter, self).remove_handlers()
+ #if an event was connected, disconnect it
+ if self._handler_id:
+ self._widget.handler_disconnect(self._handler_id)
+ self._handler_id=None
+
+ def __keypress_cb(self, widget, event, *args):
+ """keypress callback"""
+ logger.debug("~~~keypressed!")
+ key = event.keyval
+ keystr = event.string
+ logger.debug("~~~Got key: " + str(key) + ":"+ keystr)
+ self._capstrokes += [key]
+ #TODO Treat other stuff, such as arrows
+ if key == gtk.keysyms.BackSpace:
+ self._captext = self._captext[:-1]
+ else:
+ self._captext = self._captext + keystr
+
+ logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext))
+ if not self.strokes is None and self.strokes in self._capstrokes:
+ self.do_callback()
+ if not self.text is None and self.text in self._captext:
+ self.do_callback()
+
+__event__ = {
+ 'name' : 'GtkWidgetTypeFilter',
+ 'display_name' : 'Widget Filter',
+ 'icon' : '',
+ 'class' : GtkWidgetTypeFilter,
+ 'mandatory_props' : ['next_state', 'object_id']
+}
diff --git a/addons/oncewrapper.py b/addons/oncewrapper.py
new file mode 100644
index 0000000..97f4752
--- /dev/null
+++ b/addons/oncewrapper.py
@@ -0,0 +1,59 @@
+# 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 *
+
+class OnceWrapper(Action):
+ """
+ Wraps a class to perform an action once only
+
+ This ConcreteActions's do() method will only be called on the first do()
+ and the undo() will be callable after do() has been called
+ """
+
+ action = TAddonProperty()
+
+ def __init__(self, action):
+ Action.__init__(self)
+ self._called = False
+ self._need_undo = False
+ self.action = action
+
+ def do(self):
+ """
+ Do the action only on the first time
+ """
+ if not self._called:
+ self._called = True
+ self.action.do()
+ self._need_undo = True
+
+ def undo(self):
+ """
+ Undo the action if it's been done
+ """
+ if self._need_undo:
+ self.action.undo()
+ self._need_undo = False
+
+
+__action__ = {
+ 'name' : 'OnceWrapper',
+ 'display_name' : 'Execute an action only once',
+ 'icon' : 'once_wrapper',
+ 'class' : OnceWrapper,
+ 'mandatory_props' : ['action']
+}
diff --git a/addons/readfile.py b/addons/readfile.py
new file mode 100644
index 0000000..0d276b9
--- /dev/null
+++ b/addons/readfile.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+
+from sugar.tutorius.actions import Action
+from sugar.tutorius.properties import TFileProperty
+from sugar.tutorius.services import ObjectStore
+
+class ReadFile(Action):
+ filename = TFileProperty(None)
+
+ def __init__(self, filename=None):
+ """
+ Calls activity.read_file to restore a specified state to an activity
+ like when restored from the journal.
+ @param filename Path to the file to read
+ """
+ Action.__init__(self)
+
+ if filename:
+ self.filename=filename
+
+ def do(self):
+ """
+ Perform the action, call read_file on the activity
+ """
+ if os.path.isfile(str(self.filename)):
+ ObjectStore().activity.read_file(self.filename)
+
+ def undo(self):
+ """
+ Not undoable
+ """
+ pass
+
+__action__ = {
+ "name" : "ReadFile",
+ "display_name" : "Read File",
+ "icon" : "message-bubble", #FIXME
+ "class" : ReadFile,
+ "mandatory_props" : ["filename"]
+}
diff --git a/addons/timerevent.py b/addons/timerevent.py
new file mode 100644
index 0000000..c7374d0
--- /dev/null
+++ b/addons/timerevent.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gobject
+
+from sugar.tutorius.filters import EventFilter
+from sugar.tutorius.properties import TIntProperty
+
+class TimerEvent(EventFilter):
+ """
+ TimerEvent is a special EventFilter that uses gobject
+ timeouts to trigger a state change after a specified amount
+ of time. It must be used inside a gobject main loop to work.
+ """
+ timeout = TIntProperty(15, 0)
+
+ def __init__(self, timeout=None):
+ """Constructor.
+
+ @param timeout timeout in seconds
+ """
+ super(TimerEvent,self).__init__()
+ if timeout:
+ self.timeout = timeout
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install_handlers creates the timer and starts it"""
+ super(TimerEvent,self).install_handlers(callback, **kwargs)
+ #Create the timer
+ self._handler_id = gobject.timeout_add_seconds(self.timeout, self._timeout_cb)
+
+ def remove_handlers(self):
+ """remove handler removes the timer"""
+ super(TimerEvent,self).remove_handlers()
+ if self._handler_id:
+ try:
+ #XXX What happens if this was already triggered?
+ #remove the timer
+ gobject.source_remove(self._handler_id)
+ except:
+ pass
+
+ def _timeout_cb(self):
+ """
+ _timeout_cb triggers the eventfilter callback.
+
+ It is necessary because gobject timers only stop if the callback they
+ trigger returns False
+ """
+ self.do_callback()
+ return False #Stops timeout
+
+__event__ = {
+ "name" : "TimerEvent",
+ "display_name" : "Timed transition",
+ "icon" : "clock",
+ "class" : TimerEvent,
+ "mandatory_props" : ["timeout"]
+}
diff --git a/addons/triggereventfilter.py b/addons/triggereventfilter.py
new file mode 100644
index 0000000..acc0d0d
--- /dev/null
+++ b/addons/triggereventfilter.py
@@ -0,0 +1,46 @@
+# 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.filters import *
+from sugar.tutorius.properties import *
+
+class TriggerEventFilter(EventFilter):
+ """
+ This event filter can be triggered by simply calling its do_callback function.
+
+ Used to fake events and see the effect on the FSM.
+ """
+ def __init__(self):
+ EventFilter.__init__(self)
+ self.toggle_on_callback = False
+
+ def install_handlers(self, callback, **kwargs):
+ """
+ Forsakes the incoming callback function and just set the inner one.
+ """
+ self._callback = self._inner_cb
+
+ def _inner_cb(self, event_filter):
+ self.toggle_on_callback = not self.toggle_on_callback
+
+__event__ = {
+ 'name' : 'TriggerEventFilter',
+ 'display_name' : 'Triggerable event filter (test only)',
+ 'icon' : '',
+ 'class' : TriggerEventFilter,
+ 'mandatory_props' : ['next_state'],
+ 'test' : True
+}
diff --git a/addons/typetextaction.py b/addons/typetextaction.py
new file mode 100644
index 0000000..fee66e5
--- /dev/null
+++ b/addons/typetextaction.py
@@ -0,0 +1,57 @@
+# 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 *
+from sugar.tutorius import gtkutils
+
+class TypeTextAction(Action):
+ """
+ Simulate a user typing text in a widget
+ Work on any widget that implements a insert_text method
+
+ @param widget The treehish representation of the widget
+ @param text the text that is typed
+ """
+ widget = TStringProperty("")
+ text = TStringProperty("")
+
+ def __init__(self, widget, text):
+ Action.__init__(self)
+
+ self.widget = widget
+ self.text = text
+
+ def do(self, **kwargs):
+ """
+ Type the text
+ """
+ widget = gtkutils.find_widget(ObjectStore().activity, self.widget)
+ if hasattr(widget, "insert_text"):
+ widget.insert_text(self.text, -1)
+
+ def undo(self):
+ """
+ no undo
+ """
+ pass
+
+__action__ = {
+ 'name' : 'TypeTextAction',
+ 'display_name' : 'Type text',
+ 'icon' : 'format-justify-center',
+ 'class' : TypeTextAction,
+ 'mandatory_props' : ['widgetUAM', 'text']
+}
diff --git a/addons/widgetidentifyaction.py b/addons/widgetidentifyaction.py
new file mode 100644
index 0000000..3c66211
--- /dev/null
+++ b/addons/widgetidentifyaction.py
@@ -0,0 +1,47 @@
+# 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 *
+
+from sugar.tutorius.editor import WidgetIdentifier
+
+class WidgetIdentifyAction(Action):
+ def __init__(self):
+ Action.__init__(self)
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = WidgetIdentifier(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : 'WidgetIdentifyAction',
+ "display_name" : 'Widget Identifier',
+ "icon" : 'viewmag1',
+ "class" : WidgetIdentifyAction,
+ "mandatory_props" : [],
+ 'test' : True
+}
diff --git a/data/icons/chain.svg b/data/icons/chain.svg
new file mode 100644
index 0000000..e268f4f
--- /dev/null
+++ b/data/icons/chain.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48px"
+ height="48px"
+ id="svg2597"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="chain.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
+ <defs
+ id="defs2599">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective2605" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="7"
+ inkscape:cx="33.017737"
+ inkscape:cy="33.013898"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:window-width="645"
+ inkscape:window-height="726"
+ inkscape:window-x="625"
+ inkscape:window-y="25" />
+ <metadata
+ id="metadata2602">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <path
+ d="M 9.8339096,17.521424 L 11.225374,16.510468 L 21.895596,24.262843 L 20.957347,27.150437 L 19.395745,28.285012 L 20.561807,24.696201 L 11.225374,17.912898 L 10.202552,18.656048 L 9.8339096,17.521424 z M 21.057049,9.3673619 L 31.727272,1.6149871 L 42.397446,9.3673619 L 39.590553,18.005887 L 38.62545,17.304719 L 41.063657,9.8007699 L 31.727272,3.0174177 L 22.390838,9.8007699 L 23.597982,13.515988 L 23.001478,15.351781 L 21.057049,9.3673619 z M 24.194484,19.023319 L 24.790989,17.187573 L 25.957048,20.776336 L 37.497447,20.776336 L 37.888118,19.573873 L 38.853268,20.275089 L 38.321764,21.910912 L 25.132685,21.910912 L 24.194484,19.023319 z M 0.5551853,24.262843 L 7.9036154,18.923855 L 8.2723049,20.058432 L 1.8889674,24.696201 L 5.4551984,35.671819 L 16.995595,35.671819 L 18.202737,31.9566 L 19.764434,30.821973 L 17.819959,36.806394 L 4.6308143,36.806394 L 0.5551853,24.262843 z M 31.476438,20.209046 L 36.567095,16.510468 L 47.237269,24.262843 L 43.161633,36.806394 L 34.078493,36.806394 L 34.447138,35.671819 L 42.337264,35.671819 L 45.903479,24.696201 L 36.567095,17.912898 L 33.406728,20.209046 L 31.476438,20.209046 z M 5.3950189,9.3673619 L 16.065194,1.6149871 L 23.413707,6.9539745 L 22.44856,7.6551909 L 16.065194,3.0174177 L 6.7288079,9.8007699 L 10.29502,20.776336 L 14.201416,20.776336 L 15.763019,21.910912 L 9.4707017,21.910912 L 5.3950189,9.3673619 z M 18.061955,20.776336 L 21.835416,20.776336 L 25.401627,9.8007699 L 24.378759,9.0576215 L 25.343953,8.3564062 L 26.735416,9.3673619 L 22.659781,21.910912 L 19.623558,21.910912 L 18.061955,20.776336 z M 14.494797,37.37368 L 15.687758,37.37368 L 18.126011,44.877733 L 29.666452,44.877733 L 33.232667,33.902157 L 30.072253,31.605965 L 29.475748,29.770175 L 34.566456,33.468751 L 30.490816,46.012316 L 17.301647,46.012316 L 14.494797,37.37368 z M 25.896871,24.262843 L 28.353226,22.478199 L 30.283475,22.478199 L 27.230659,24.696201 L 30.796869,35.671819 L 32.061166,35.671819 L 31.692527,36.806394 L 29.972505,36.806394 L 25.896871,24.262843 z M 13.226011,33.468751 L 23.896234,25.716376 L 26.352544,27.501018 L 26.949047,29.336809 L 23.896234,27.118808 L 14.559799,33.902157 L 14.950472,35.104529 L 13.757512,35.104529 L 13.226011,33.468751 z"
+ id="path2535" />
+ </g>
+</svg>
diff --git a/data/icons/clock.svg b/data/icons/clock.svg
new file mode 100644
index 0000000..dc73bbb
--- /dev/null
+++ b/data/icons/clock.svg
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="51" height="51" id="svg2" sodipodi:version="0.32" inkscape:version="0.44+devel" sodipodi:docbase="C:\Documents and Settings\Molumen\Desktop" sodipodi:docname="clock_beige.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" sodipodi:modified="true" version="1.0">
+ <defs id="defs4">
+ <linearGradient y2="84.524567" x2="302" y1="365.95651" x1="302" gradientUnits="userSpaceOnUse" id="linearGradient20470" xlink:href="#linearGradient13034" inkscape:collect="always"/>
+ <radialGradient r="90.78125" fy="691.20294" fx="527" cy="691.20294" cx="527" gradientTransform="matrix(1, 0, 0, 0.231842, -340, 200.219)" gradientUnits="userSpaceOnUse" id="radialGradient20468" xlink:href="#linearGradient12977" inkscape:collect="always"/>
+ <radialGradient r="113.53125" fy="368.17188" fx="528" cy="368.17188" cx="528" gradientTransform="matrix(1, 0, 0, 0.262455, -341, 27.5432)" gradientUnits="userSpaceOnUse" id="radialGradient20466" xlink:href="#linearGradient12977" inkscape:collect="always"/>
+ <radialGradient r="2.625" fy="468.57623" fx="504.125" cy="468.57623" cx="504.125" gradientTransform="matrix(1.05261, 0, 0, 1.05261, -26.5224, -23.8951)" gradientUnits="userSpaceOnUse" id="radialGradient20464" xlink:href="#linearGradient13172" inkscape:collect="always"/>
+ <radialGradient r="138" fy="467.18744" fx="525.49945" cy="467.18744" cx="525.49945" spreadMethod="pad" gradientTransform="matrix(1.77314, 0, 0, 1.77314, -744.784, -597.014)" gradientUnits="userSpaceOnUse" id="radialGradient20462" xlink:href="#linearGradient12953" inkscape:collect="always"/>
+ <radialGradient r="138" fy="239.93021" fx="302" cy="239.93021" cx="302" gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" gradientUnits="userSpaceOnUse" id="radialGradient20460" xlink:href="#linearGradient20428" inkscape:collect="always"/>
+ <radialGradient r="138" fy="239.93021" fx="302" cy="239.93021" cx="302" gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" gradientUnits="userSpaceOnUse" id="radialGradient20438" xlink:href="#linearGradient20428" inkscape:collect="always"/>
+ <radialGradient r="138" fy="467.18744" fx="525.49945" cy="467.18744" cx="525.49945" spreadMethod="pad" gradientTransform="matrix(1.77314, 0, 0, 1.77314, -744.784, -597.014)" gradientUnits="userSpaceOnUse" id="radialGradient19456" xlink:href="#linearGradient12953" inkscape:collect="always"/>
+ <radialGradient r="113.53125" fy="368.17188" fx="528" cy="368.17188" cx="528" gradientTransform="matrix(1, 0, 0, 0.262455, -341, 27.5432)" gradientUnits="userSpaceOnUse" id="radialGradient19449" xlink:href="#linearGradient12977" inkscape:collect="always"/>
+ <radialGradient r="90.78125" fy="691.20294" fx="527" cy="691.20294" cx="527" gradientTransform="matrix(1, 0, 0, 0.231842, -340, 200.219)" gradientUnits="userSpaceOnUse" id="radialGradient19446" xlink:href="#linearGradient12977" inkscape:collect="always"/>
+ <linearGradient y2="84.524567" x2="302" y1="365.95651" x1="302" gradientUnits="userSpaceOnUse" id="linearGradient19441" xlink:href="#linearGradient13034" inkscape:collect="always"/>
+ <radialGradient r="90.78125" fy="691.20294" fx="527" cy="691.20294" cx="527" gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" gradientUnits="userSpaceOnUse" id="radialGradient19439" xlink:href="#linearGradient12977" inkscape:collect="always"/>
+ <radialGradient r="113.53125" fy="368.17188" fx="528" cy="368.17188" cx="528" gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" gradientUnits="userSpaceOnUse" id="radialGradient19437" xlink:href="#linearGradient12977" inkscape:collect="always"/>
+ <radialGradient r="2.625" fy="468.57623" fx="504.125" cy="468.57623" cx="504.125" gradientTransform="matrix(1.05261, 0, 0, 1.05261, -26.5224, -23.8951)" gradientUnits="userSpaceOnUse" id="radialGradient19435" xlink:href="#linearGradient13172" inkscape:collect="always"/>
+ <radialGradient r="138" fy="467.18744" fx="525.49945" cy="467.18744" cx="525.49945" spreadMethod="pad" gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" gradientUnits="userSpaceOnUse" id="radialGradient19433" xlink:href="#linearGradient12953" inkscape:collect="always"/>
+ <radialGradient r="138" fy="239.93021" fx="302" cy="239.93021" cx="302" gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" gradientUnits="userSpaceOnUse" id="radialGradient19431" xlink:href="#linearGradient13012" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16295" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -781.919, -183.605)" gradientUnits="userSpaceOnUse" id="radialGradient16293" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16285" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -781.919, -183.605)" gradientUnits="userSpaceOnUse" id="radialGradient16283" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16275" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -781.919, -183.605)" gradientUnits="userSpaceOnUse" id="radialGradient16273" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16265" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -781.919, -183.605)" gradientUnits="userSpaceOnUse" id="radialGradient16263" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16249" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -781.919, -183.605)" gradientUnits="userSpaceOnUse" id="radialGradient16241" xlink:href="#linearGradient16243" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16197" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16195" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16189" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16187" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16181" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16179" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16173" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16171" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16165" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16163" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16095" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, -173.605)" gradientUnits="userSpaceOnUse" id="radialGradient16093" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16091" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16089" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16087" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, -173.605)" gradientUnits="userSpaceOnUse" id="radialGradient16085" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16083" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16081" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16079" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, -173.605)" gradientUnits="userSpaceOnUse" id="radialGradient16077" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16075" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16073" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16071" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, -173.605)" gradientUnits="userSpaceOnUse" id="radialGradient16069" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16067" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16065" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16063" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, -173.605)" gradientUnits="userSpaceOnUse" id="radialGradient16061" xlink:href="#linearGradient15913" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" gradientUnits="userSpaceOnUse" id="radialGradient16059" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient r="131" fy="361.61154" fx="296.26508" cy="361.61154" cx="296.26508" gradientTransform="matrix(1.25538, -1.25538, 2.63349, 2.63349, -1015.92, 24.3952)" gradientUnits="userSpaceOnUse" id="radialGradient16057" xlink:href="#linearGradient3346" inkscape:collect="always"/>
+ <radialGradient xlink:href="#linearGradient10759" cx="202.5" cy="578.86218" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="578.86218" fx="202.5" id="radialGradient13728" r="91.5"/>
+ <linearGradient y2="84.524567" y1="365.95651" xlink:href="#linearGradient13034" x2="302" x1="302" inkscape:collect="always" gradientUnits="userSpaceOnUse" id="linearGradient13265"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" xlink:href="#linearGradient12977" cx="527" cy="691.20294" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="691.20294" fx="527" id="radialGradient13263" r="90.78125"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient13261" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1.05261, 0, 0, 1.05261, -26.5224, -23.8951)" xlink:href="#linearGradient13172" cx="504.125" cy="468.57623" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="468.57623" fx="504.125" id="radialGradient13259" r="2.625"/>
+ <radialGradient gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" xlink:href="#linearGradient12953" cx="525.49945" cy="467.18744" inkscape:collect="always" gradientUnits="userSpaceOnUse" spreadMethod="pad" fy="467.18744" fx="525.49945" id="radialGradient13257" r="138"/>
+ <radialGradient gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" xlink:href="#linearGradient13012" cx="302" cy="239.93021" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="239.93021" fx="302" id="radialGradient13255" r="138"/>
+ <linearGradient y2="84.524567" y1="365.95651" xlink:href="#linearGradient13034" x2="302" x1="302" inkscape:collect="always" gradientUnits="userSpaceOnUse" id="linearGradient13231"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" xlink:href="#linearGradient12977" cx="527" cy="691.20294" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="691.20294" fx="527" id="radialGradient13229" r="90.78125"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient13227" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1.05261, 0, 0, 1.05261, -26.5224, -23.8951)" xlink:href="#linearGradient13172" cx="504.125" cy="468.57623" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="468.57623" fx="504.125" id="radialGradient13225" r="2.625"/>
+ <radialGradient gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" xlink:href="#linearGradient12953" cx="525.49945" cy="467.18744" inkscape:collect="always" gradientUnits="userSpaceOnUse" spreadMethod="pad" fy="467.18744" fx="525.49945" id="radialGradient13223" r="138"/>
+ <radialGradient gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" xlink:href="#linearGradient13012" cx="302" cy="239.93021" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="239.93021" fx="302" id="radialGradient13221" r="138"/>
+ <radialGradient gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" xlink:href="#linearGradient12953" cx="525.49945" cy="467.18744" inkscape:collect="always" gradientUnits="userSpaceOnUse" spreadMethod="pad" fy="467.18744" fx="525.49945" id="radialGradient13206" r="138"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient13203" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" xlink:href="#linearGradient12977" cx="527" cy="691.20294" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="691.20294" fx="527" id="radialGradient13200" r="90.78125"/>
+ <linearGradient y2="84.524567" y1="365.95651" xlink:href="#linearGradient13034" x2="302" x1="302" inkscape:collect="always" gradientUnits="userSpaceOnUse" id="linearGradient13195"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" xlink:href="#linearGradient12977" cx="527" cy="691.20294" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="691.20294" fx="527" id="radialGradient13193" r="90.78125"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient13191" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" xlink:href="#linearGradient12953" cx="525.49945" cy="467.18744" inkscape:collect="always" gradientUnits="userSpaceOnUse" spreadMethod="pad" fy="467.18744" fx="525.49945" id="radialGradient13189" r="138"/>
+ <radialGradient gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" xlink:href="#linearGradient13012" cx="302" cy="239.93021" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="239.93021" fx="302" id="radialGradient13187" r="138"/>
+ <radialGradient gradientTransform="matrix(1.05261, 0, 0, 1.05261, -26.5224, -23.8951)" xlink:href="#linearGradient13172" cx="504.125" cy="468.57623" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="468.57623" fx="504.125" id="radialGradient13170" r="2.625"/>
+ <linearGradient y2="84.524567" y1="365.95651" xlink:href="#linearGradient13034" x2="302" x1="302" inkscape:collect="always" gradientUnits="userSpaceOnUse" id="linearGradient13146"/>
+ <radialGradient gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" xlink:href="#linearGradient12953" cx="525.49945" cy="467.18744" inkscape:collect="always" gradientUnits="userSpaceOnUse" spreadMethod="pad" fy="467.18744" fx="525.49945" id="radialGradient13143" r="138"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient13140" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" xlink:href="#linearGradient12977" cx="527" cy="691.20294" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="691.20294" fx="527" id="radialGradient13137" r="90.78125"/>
+ <linearGradient y2="150.36218" y1="426.36218" xlink:href="#linearGradient13034" x2="302" x1="302" inkscape:collect="always" gradientUnits="userSpaceOnUse" id="linearGradient13133"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" xlink:href="#linearGradient12977" cx="527" cy="691.20294" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="691.20294" fx="527" id="radialGradient13131" r="90.78125"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient13129" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" xlink:href="#linearGradient12953" cx="525.49945" cy="467.18744" inkscape:collect="always" gradientUnits="userSpaceOnUse" spreadMethod="pad" fy="467.18744" fx="525.49945" id="radialGradient13127" r="138"/>
+ <radialGradient gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" xlink:href="#linearGradient13012" cx="302" cy="239.93021" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="239.93021" fx="302" id="radialGradient13125" r="138"/>
+ <linearGradient y2="150.36218" y1="426.36218" xlink:href="#linearGradient13034" x2="302" x1="302" inkscape:collect="always" gradientUnits="userSpaceOnUse" id="linearGradient13032"/>
+ <radialGradient gradientTransform="matrix(3.14096, 0, 0, 3.14096, -646.57, -549.905)" xlink:href="#linearGradient13012" cx="302" cy="239.93021" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="239.93021" fx="302" id="radialGradient13010" r="138"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.231842, -1, 463.219)" xlink:href="#linearGradient12977" cx="527" cy="691.20294" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="691.20294" fx="527" id="radialGradient13000" r="90.78125"/>
+ <radialGradient gradientTransform="matrix(-0.932879, 0, 0, -0.244839, 1018.94, 683.505)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient12987" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1, 0, 0, 0.262455, -2, 290.543)" xlink:href="#linearGradient12977" cx="528" cy="368.17188" inkscape:collect="always" gradientUnits="userSpaceOnUse" fy="368.17188" fx="528" id="radialGradient12983" r="113.53125"/>
+ <radialGradient gradientTransform="matrix(1.77314, 0, 0, 1.77314, -405.784, -334.014)" xlink:href="#linearGradient12953" cx="525.49945" cy="467.18744" inkscape:collect="always" gradientUnits="userSpaceOnUse" spreadMethod="pad" fy="467.18744" fx="525.49945" id="radialGradient12959" r="138"/>
+ <linearGradient id="linearGradient5553">
+ <stop id="stop5555" style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" offset="0"/>
+ <stop id="stop5557" style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5613">
+ <stop id="stop5615" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.888889;" offset="0"/>
+ <stop id="stop5617" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.5"/>
+ <stop id="stop5619" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop5621" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5675">
+ <stop id="stop5677" style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" offset="0"/>
+ <stop id="stop5679" style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" offset="0.5"/>
+ <stop id="stop5681" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop5683" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5563">
+ <stop id="stop5565" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.703704;" offset="0"/>
+ <stop id="stop5571" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.189815;" offset="0.50850612"/>
+ <stop id="stop5573" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop5567" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5537">
+ <stop id="stop5539" style="stop-color: rgb(74, 206, 96); stop-opacity: 1;" offset="0"/>
+ <stop id="stop5541" style="stop-color: rgb(180, 234, 135); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10743">
+ <stop id="stop10745" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.703704;" offset="0"/>
+ <stop id="stop10747" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.189815;" offset="0.50850612"/>
+ <stop id="stop10749" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop10751" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10753">
+ <stop id="stop10755" style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" offset="0"/>
+ <stop id="stop10757" style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10759">
+ <stop id="stop10761" style="stop-color: rgb(74, 206, 96); stop-opacity: 1;" offset="0"/>
+ <stop id="stop10763" style="stop-color: rgb(180, 234, 135); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10767">
+ <stop id="stop10769" style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" offset="0"/>
+ <stop id="stop10771" style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10773">
+ <stop id="stop10775" style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" offset="0"/>
+ <stop id="stop10777" style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" offset="0.5"/>
+ <stop id="stop10779" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop10781" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10783">
+ <stop id="stop10785" style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" offset="0"/>
+ <stop id="stop10787" style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" offset="0.5"/>
+ <stop id="stop10789" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop10791" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10793">
+ <stop id="stop10795" style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" offset="0"/>
+ <stop id="stop10797" style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" offset="0.5"/>
+ <stop id="stop10799" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop10801" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10803">
+ <stop id="stop10805" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.888889;" offset="0"/>
+ <stop id="stop10807" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.5"/>
+ <stop id="stop10809" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0.51142859"/>
+ <stop id="stop10811" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10813">
+ <stop id="stop10815" style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" offset="0"/>
+ <stop id="stop10817" style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10819">
+ <stop id="stop10821" style="stop-color: rgb(74, 206, 96); stop-opacity: 1;" offset="0"/>
+ <stop id="stop10823" style="stop-color: rgb(180, 234, 135); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient12953">
+ <stop id="stop12955" style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" offset="0"/>
+ <stop id="stop12965" style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" offset="0.47816542"/>
+ <stop id="stop12961" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0.49808899"/>
+ <stop id="stop12967" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0.50756544"/>
+ <stop id="stop12963" style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" offset="0.53007674"/>
+ <stop id="stop12957" style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient12977">
+ <stop id="stop12979" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.319444;" offset="0"/>
+ <stop id="stop12981" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient13012">
+ <stop id="stop13014" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0"/>
+ <stop id="stop13018" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0.20165709"/>
+ <stop id="stop13020" style="stop-color: rgb(239, 245, 251); stop-opacity: 1;" offset="0.32675916"/>
+ <stop id="stop13016" style="stop-color: rgb(4, 72, 127); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient13034">
+ <stop id="stop13036" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0"/>
+ <stop id="stop13038" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient13172">
+ <stop id="stop13174" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0"/>
+ <stop id="stop13176" style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3346">
+ <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0" id="stop3348"/>
+ <stop style="stop-color: rgb(233, 233, 233); stop-opacity: 1;" offset="1" id="stop3350"/>
+ </linearGradient>
+ <filter id="filter4767" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur4769" stdDeviation="0.77125" inkscape:collect="always"/>
+ </filter>
+ <linearGradient id="linearGradient3916">
+ <stop style="stop-color: rgb(139, 139, 139); stop-opacity: 0.639175;" offset="0" id="stop3918"/>
+ <stop id="stop3924" offset="0.44642857" style="stop-color: rgb(141, 141, 141); stop-opacity: 0.206186;"/>
+ <stop id="stop3922" offset="1" style="stop-color: rgb(143, 143, 143); stop-opacity: 0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3440">
+ <stop style="stop-color: black; stop-opacity: 1;" offset="0" id="stop3442"/>
+ <stop id="stop3452" offset="0.3125" style="stop-color: black; stop-opacity: 1;"/>
+ <stop id="stop3446" offset="0.53727454" style="stop-color: white; stop-opacity: 1;"/>
+ <stop style="stop-color: white; stop-opacity: 1;" offset="0.60522962" id="stop3542"/>
+ <stop style="stop-color: black; stop-opacity: 1;" offset="0.6964286" id="stop3448"/>
+ <stop style="stop-color: black; stop-opacity: 1;" offset="1" id="stop3444"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3757">
+ <stop id="stop3759" offset="0" style="stop-color: black; stop-opacity: 1;"/>
+ <stop id="stop3761" offset="1" style="stop-color: black; stop-opacity: 0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3584">
+ <stop id="stop3586" offset="0" style="stop-color: white; stop-opacity: 1;"/>
+ <stop style="stop-color: white; stop-opacity: 0.498039;" offset="1" id="stop3592"/>
+ <stop id="stop3588" offset="1" style="stop-color: white; stop-opacity: 0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient15913">
+ <stop id="stop15915" offset="0" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;"/>
+ <stop id="stop15917" offset="1" style="stop-color: rgb(240, 216, 35); stop-opacity: 1;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient16243">
+ <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0" id="stop16245"/>
+ <stop style="stop-color: rgb(35, 178, 240); stop-opacity: 1;" offset="1" id="stop16247"/>
+ </linearGradient>
+ <linearGradient id="linearGradient20428">
+ <stop offset="0" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" id="stop20430"/>
+ <stop offset="0.20165709" style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" id="stop20432"/>
+ <stop offset="0.32675916" style="stop-color: rgb(251, 248, 239); stop-opacity: 1;" id="stop20434"/>
+ <stop offset="1" style="stop-color: rgb(127, 98, 4); stop-opacity: 1;" id="stop20436"/>
+ </linearGradient>
+ <radialGradient inkscape:collect="always" xlink:href="#linearGradient3346" id="radialGradient2855" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.03888, -1.98608, 4.32211, 2.26874, -1497.58, 2.13654)" cx="296.26508" cy="361.61154" fx="296.26508" fy="361.61154" r="131"/>
+ </defs>
+ <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" gridtolerance="10000" guidetolerance="10" objecttolerance="10" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.49497475" inkscape:cx="448.89881" inkscape:cy="364.07566" inkscape:document-units="px" inkscape:current-layer="layer1" inkscape:window-width="1208" inkscape:window-height="1070" inkscape:window-x="222" inkscape:window-y="20"/>
+ <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"/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-436.343, -114.661)">
+ <g transform="matrix(0.836957, 0, 0, 0.836957, 111.604, -183.61)" id="g19409">
+ <path sodipodi:ry="138" sodipodi:rx="138" transform="matrix(0.862319, 0, 0, 0.862319, 265.58, 245.715)" d="M 440,288.36218 A 138,138 0 1 1 164,288.36218 A 138,138 0 1 1 440,288.36218 z" sodipodi:type="arc" sodipodi:cy="288.36218" sodipodi:cx="302" id="path19411" style="fill: url(#radialGradient19431) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ <path d="M 526,356.375 C 449.824,356.375 388,418.199 388,494.375 C 388,570.551 449.824,632.375 526,632.375 C 602.176,632.375 664,570.551 664,494.375 C 664,418.199 602.176,356.375 526,356.375 z M 526,375.375 C 591.688,375.375 645,428.687 645,494.375 C 644.99999,560.063 591.688,613.375 526,613.375 C 460.312,613.37499 407,560.063 407,494.375 C 407,428.687 460.312,375.375 526,375.375 z" id="path19413" style="fill: url(#radialGradient19433) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ <g id="g19415">
+ <path id="path19417" sodipodi:nodetypes="ccccc" d="M 572.45442,416.66652 L 574.56216,417.96362 L 518.94889,510.35634 L 515.03453,507.94743 L 572.45442,416.66652 z" style="fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ <path id="path19419" sodipodi:nodetypes="ccccc" d="M 470.10131,522.52645 L 468.3205,518.67564 L 539.27256,484.69517 L 541.94377,490.47138 L 470.10131,522.52645 z" style="fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ <path sodipodi:ry="2.625" sodipodi:rx="2.625" transform="matrix(1.85714, 0, 0, 1.85714, -410.232, -381.244)" d="M 506.75,471.48718 A 2.625,2.625 0 1 1 501.5,471.48718 A 2.625,2.625 0 1 1 506.75,471.48718 z" sodipodi:type="arc" sodipodi:cy="471.48718" sodipodi:cx="504.125" id="path19421" style="opacity: 1; fill: url(#radialGradient19435) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ </g>
+ <path d="M 526,357.375 C 478.94483,357.375 437.38188,380.97729 412.46875,416.96875 C 441.06326,387.02851 481.36166,368.375 526,368.375 C 570.63834,368.375 610.93674,387.02852 639.53125,416.96875 C 614.61812,380.97729 573.05517,357.375 526,357.375 z" id="path19423" style="fill: url(#radialGradient19437) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ <path d="M 526,623.46875 C 562.37673,623.46875 594.94742,607.12155 616.78125,581.375 C 592.50974,602.60542 560.7553,615.46875 526,615.46875 C 491.2447,615.46875 459.49026,602.60542 435.21875,581.375 C 457.05258,607.12155 489.62327,623.46875 526,623.46875 z" id="path19425" style="fill: url(#radialGradient19439) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ <path d="M 523.53125,378.40625 L 523.53125,402.125 C 525.1769,402.07398 526.82304,402.09578 528.46875,402.125 L 528.46875,378.40625 L 523.53125,378.40625 z M 468.9375,393.40625 L 467.09375,394.46875 L 477.0625,411.75 C 477.67174,411.38819 478.28844,411.03612 478.90625,410.6875 L 468.9375,393.40625 z M 583.0625,393.40625 L 573.09375,410.6875 C 573.71156,411.03612 574.32826,411.38819 574.9375,411.75 L 584.90625,394.46875 L 583.0625,393.40625 z M 426.09375,435.46875 L 425.03125,437.3125 L 442.3125,447.28125 C 442.66112,446.66344 443.01319,446.04674 443.375,445.4375 L 426.09375,435.46875 z M 625.90625,435.46875 L 608.625,445.4375 C 608.98681,446.04674 609.33888,446.66344 609.6875,447.28125 L 626.96875,437.3125 L 625.90625,435.46875 z M 410.03125,491.90625 L 410.03125,496.84375 L 433.75,496.84375 C 433.69898,495.1981 433.72078,493.55196 433.75,491.90625 L 410.03125,491.90625 z M 618.25,491.90625 C 618.30102,493.5519 618.27922,495.19804 618.25,496.84375 L 641.96875,496.84375 L 641.96875,491.90625 L 618.25,491.90625 z M 442.3125,541.46875 L 425.03125,551.4375 L 426.09375,553.28125 L 443.375,543.3125 C 443.01319,542.70326 442.66112,542.08656 442.3125,541.46875 z M 609.6875,541.46875 C 609.33888,542.08656 608.98681,542.70326 608.625,543.3125 L 625.90625,553.28125 L 626.96875,551.4375 L 609.6875,541.46875 z M 477.0625,577 L 467.09375,594.28125 L 468.9375,595.34375 L 478.90625,578.0625 C 478.28844,577.71388 477.67174,577.36181 477.0625,577 z M 574.9375,577 C 574.32826,577.36181 573.71156,577.71388 573.09375,578.0625 L 583.0625,595.34375 L 584.90625,594.28125 L 574.9375,577 z M 523.53125,586.625 L 523.53125,610.34375 L 528.46875,610.34375 L 528.46875,586.625 C 526.8231,586.67602 525.17696,586.65422 523.53125,586.625 z" sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" id="path19427" style="fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ <path sodipodi:ry="138" sodipodi:rx="138" transform="matrix(0.728261, 0, 0, 0.601449, 306.065, 286.927)" d="M 440,288.36218 A 138,138 0 1 1 164,288.36218 A 138,138 0 1 1 440,288.36218 z" sodipodi:type="arc" sodipodi:cy="288.36218" sodipodi:cx="302" id="path19429" style="fill: url(#linearGradient19441) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1;"/>
+ </g>
+ </g>
+</svg>
diff --git a/data/icons/once_wrapper.svg b/data/icons/once_wrapper.svg
new file mode 100644
index 0000000..ad48720
--- /dev/null
+++ b/data/icons/once_wrapper.svg
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48px"
+ height="48px"
+ id="svg2393"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="once_wrapper.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
+ <defs
+ id="defs2395">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective2401" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="7"
+ inkscape:cx="24"
+ inkscape:cy="24"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:window-width="645"
+ inkscape:window-height="726"
+ inkscape:window-x="625"
+ inkscape:window-y="25" />
+ <metadata
+ id="metadata2398">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <flowRoot
+ xml:space="preserve"
+ id="flowRoot2403"
+ style="font-size:40px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:OpenSymbol;-inkscape-font-specification:OpenSymbol"
+ transform="matrix(1.2841451,0,0,1.2841451,-10.095321,-10.594455)"><flowRegion
+ id="flowRegion2405"><rect
+ id="rect2407"
+ width="45"
+ height="45"
+ x="1.8571428"
+ y="1.4285715"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:OpenSymbol;-inkscape-font-specification:OpenSymbol" /></flowRegion><flowPara
+ id="flowPara2409"> 1</flowPara></flowRoot> </g>
+</svg>
diff --git a/setup.cfg b/setup.cfg.example
index e33c680..e33c680 100644
--- a/setup.cfg
+++ b/setup.cfg.example
diff --git a/setup.py b/setup.py
index 9362dc7..23e8532 100755
--- a/setup.py
+++ b/setup.py
@@ -98,6 +98,7 @@ setup(name='Tutorius',
'sugar.tutorius': 'tutorius',
'sugar.tutorius.addons': 'addons',
},
+ package_data={'sugar.tutorius': ['ui/*.glade']},
cmdclass = {'test': TestCommand},
data_files=[('share/icons/sugar/scalable/actions', glob.glob('data/icons/*.svg')),]
)
diff --git a/tests/actiontests.py b/tests/actiontests.py
index 4e126b3..7b8d1cb 100644
--- a/tests/actiontests.py
+++ b/tests/actiontests.py
@@ -25,6 +25,7 @@ import unittest
import gtk
from sugar.tutorius import addon
+from sugar.tutorius.addons.triggereventfilter import *
from sugar.tutorius.actions import *
from sugar.tutorius.services import ObjectStore
@@ -65,7 +66,7 @@ class DialogMessageTest(unittest.TestCase):
class BubbleMessageTest(unittest.TestCase):
def setUp(self):
- self.bubble = addon.create('BubbleMessage', message="Message text", pos=[200, 300], tailpos=[-15, -25])
+ self.bubble = addon.create('BubbleMessage', message="Message text", position=[200, 300], tail_pos=[-15, -25])
def test_properties(self):
props = self.bubble.get_properties()
@@ -115,7 +116,7 @@ class OnceWrapperTests(unittest.TestCase):
CountAction
"""
act = CountAction()
- wrap = OnceWrapper(act)
+ wrap = addon.create('OnceWrapper', act)
assert act.do_count == 0, "do() should not have been called in __init__()"
assert act.undo_count == 0, "undo() should not have been called in __init__()"
@@ -152,7 +153,7 @@ class ChainActionTest(unittest.TestCase):
def test_empty(self):
"""If the expected empty behavior (do nothing) changes
and starts throwing exceptions, this will flag it"""
- a = ChainAction()
+ a = addon.create('ChainAction')
a.do()
a.undo()
@@ -161,7 +162,7 @@ class ChainActionTest(unittest.TestCase):
first = ChainTester(witness)
second = ChainTester(witness)
- c = ChainAction(first, second)
+ c = addon.create('ChainAction', [first, second])
assert witness == [], "Actions should not be triggered on init"""
c.do()
@@ -194,13 +195,171 @@ class DisableWidgetActionTests(unittest.TestCase):
assert btn.props.sensitive is True, "Callback should have been called"
- act = DisableWidgetAction("0")
+ act = addon.create('DisableWidgetAction', "0")
assert btn.props.sensitive is True, "Callback should have been called again"
act.do()
assert btn.props.sensitive is False, "Callback should not have been called again"
act.undo()
assert btn.props.sensitive is True, "Callback should have been called again"
+class TrueWhileActiveAction(Action):
+ """
+ This action's active member is set to True after a do and to False after
+ an undo.
+
+ Used to verify that a State correctly triggers the do and undo actions.
+ """
+ def __init__(self):
+ Action.__init__(self)
+ self.active = False
+
+ def do(self):
+ self.active = True
+
+ def undo(self):
+ self.active = False
+
+class ClickableWidget():
+ """
+ This class fakes a widget with a clicked() method
+ """
+ def __init__(self):
+ self.click_count = 0
+
+ def clicked(self):
+ self.click_count += 1
+
+class FakeTextEntry():
+ """
+ This class fakes a widget with an insert_text() method
+ """
+ def __init__(self):
+ self.text_lines = []
+ self.last_entered_line = ""
+ self.displayed_text = ""
+
+ def insert_text(self, text, index):
+ self.last_entered_line = text
+ self.text_lines.append(text)
+ self.displayed_text = self.displayed_text[0:index] + text + self.displayed_text[index+1:]
+
+class FakeParentWidget():
+ """
+ This class fakes a widet container, it implements the get_children() method
+ """
+ def __init__(self):
+ self._children = []
+
+ def add_child(self, child):
+ self._children.append(child)
+
+ def get_children(self):
+ return self._children
+
+class FakeEventFilter(TriggerEventFilter):
+ """
+ This is a fake event that is connected to the tutorial.
+
+ The difference between this one and the TriggerEventFilter is that the
+ tutorial's set_state will be called on the callback.
+
+ Do not forget to add the do_callback() after creating the object.
+ """
+ def set_tutorial(self, tutorial):
+ self.tutorial = tutorial
+
+ def _inner_cb(self, event_filter):
+ self.toggle_on_callback = not self.toggle_on_callback
+ self.tutorial.set_state(event_filter.get_next_state())
+
+class TypeTextActionTests(unittest.TestCase):
+ """
+ Test class for type text action
+ """
+ def test_do_action(self):
+ activity = FakeParentWidget()
+ widget = FakeTextEntry()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
+
+ test_text = "This is text"
+
+
+ action = addon.create('TypeTextAction', "0.0", test_text)
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.do()
+
+ assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
+
+ action.do()
+
+ assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
+ assert len(widget.text_lines) == 2, "insert_text() should have been called twice"
+
+ def test_undo(self):
+ activity = FakeParentWidget()
+ widget = FakeTextEntry()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
+
+ test_text = "This is text"
+
+
+ action = addon.create('TypeTextAction', "0.0", test_text)
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.undo()
+
+ #There is no undo for this action so the test should not fail
+ assert True
+
+class ClickActionTests(unittest.TestCase):
+ """
+ Test class for click action
+ """
+ def test_do_action(self):
+ activity = FakeParentWidget()
+ widget = ClickableWidget()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
+
+ action = addon.create('ClickAction', "0.0")
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.do()
+
+ assert widget.click_count == 1, "clicked() should have been called by do()"
+
+ action.do()
+
+ assert widget.click_count == 2, "clicked() should have been called by do()"
+
+ def test_undo(self):
+ activity = FakeParentWidget()
+ widget = ClickableWidget()
+ activity.add_child(widget)
+ ObjectStore().activity = activity
+
+ action = addon.create('ClickAction', "0.0")
+
+ assert widget == ObjectStore().activity.get_children()[0],\
+ "The clickable widget isn't reachable from the object store \
+ the test cannot pass"
+
+ action.undo()
+
+ #There is no undo for this action so the test should not fail
+ assert True
if __name__ == "__main__":
unittest.main()
diff --git a/tests/addontests.py b/tests/addontests.py
new file mode 100644
index 0000000..5fb4f61
--- /dev/null
+++ b/tests/addontests.py
@@ -0,0 +1,50 @@
+# 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
+
+import unittest
+
+from sugar.tutorius import addon
+
+class AddonTest(unittest.TestCase):
+ def test_create_constructor_fail(self):
+ try:
+ obj = addon.create("BubbleMessage", wrong_param=True, second_wrong="This", last_wrong=12, unknown=13.4)
+ assert False, "Constructor with wrong parameter should raise an exception"
+ except:
+ pass
+
+ def test_create_wrong_addon(self):
+ try:
+ obj = addon.create("Non existing addon name")
+ assert False, "Addon creator should raise an exception when the requested addon is unknown"
+ except:
+ pass
+
+ def test_create(self):
+ obj = addon.create("BubbleMessage", message="Hi!", position=[12,31])
+
+ assert obj is not None
+
+ def test_reload_addons(self):
+ addon._cache = None
+ assert len(addon.list_addons()) > 0, "Addons should be reloaded upon cache clear"
+
+ def test_get_addon_meta(self):
+ addon._cache = None
+ meta = addon.get_addon_meta("BubbleMessage")
+ assert meta.keys() == ['mandatory_props', 'class', 'display_name', 'name', 'icon',]
diff --git a/tests/bundlertests.py b/tests/bundlertests.py
deleted file mode 100644
index 8da2310..0000000
--- a/tests/bundlertests.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright (C) 2009, Tutorius.org
-# Copyright (C) 2009, Charles-Etienne Carriere <iso.swiffer@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
-"""
-Bundler tests
-
-This module contains all the tests for the storage mecanisms for tutorials
-This mean testing savins and loading tutorial, .ini file management and
-adding ressources to tutorial
-"""
-
-import unittest
-import os
-import uuid
-
-from sugar.tutorius import bundler
-
-class TutorialBundlerTests(unittest.TestCase):
-
- def setUp(self):
-
- #generate a test GUID
- self.test_guid = uuid.uuid1()
- self.guid_path = os.path.join(bundler._get_store_root(),str(self.test_guid))
- os.mkdir(self.guid_path)
-
- self.ini_file = os.path.join(self.guid_path, "meta.ini")
-
- f = open(self.ini_file,'w')
- f.write("[GENERAL_METADATA]")
- f.write(os.linesep)
- f.write("GUID:")
- f.write(str(self.test_guid))
- f.close()
-
- def tearDown(self):
- os.remove(self.ini_file)
- os.rmdir(self.guid_path)
-
- def test_add_ressource(self):
- bund = bundler.TutorialBundler(self.test_guid)
-
- temp_file = open("test.txt",'w')
- temp_file.write('test')
- temp_file.close()
-
- bund.add_resource("test.txt")
-
- assert os.path.exists(os.path.join(self.guid_path,"test.txt")), "add_ressource did not create the file"
-
-if __name__ == "__main__":
- unittest.main() \ No newline at end of file
diff --git a/tests/constraintstests.py b/tests/constraintstests.py
index b7b0a47..4e19a92 100644
--- a/tests/constraintstests.py
+++ b/tests/constraintstests.py
@@ -16,6 +16,9 @@
import unittest
+import uuid
+import os
+
from sugar.tutorius.constraints import *
class ConstraintTest(unittest.TestCase):
@@ -218,10 +221,18 @@ class EnumConstraintTest(unittest.TestCase):
assert False, "Wrong exception type thrown"
class FileConstraintTest(unittest.TestCase):
+ def setUp(self):
+ self.temp_filename = "sample_file_" + str(uuid.uuid1()) + ".txt"
+ self.file1 = file(self.temp_filename, "w")
+ self.file1.close()
+
+ def tearDown(self):
+ os.unlink(self.temp_filename)
+
def test_validate(self):
cons = FileConstraint()
- cons.validate("run-tests.py")
+ cons.validate(self.temp_filename)
try:
cons.validate("unknown/file.py")
@@ -230,4 +241,4 @@ class FileConstraintTest(unittest.TestCase):
pass
if __name__ == "__main__":
- unittest.main() \ No newline at end of file
+ unittest.main()
diff --git a/tests/coretests.py b/tests/coretests.py
index eadea01..b2f68e5 100644
--- a/tests/coretests.py
+++ b/tests/coretests.py
@@ -28,13 +28,14 @@ and event filters. Those are in their separate test module
import unittest
+import copy
import logging
-from sugar.tutorius.actions import Action, OnceWrapper, ClickAction, TypeTextAction
+from sugar.tutorius.actions import *
+from sugar.tutorius.addon import *
from sugar.tutorius.core import *
from sugar.tutorius.filters import *
-
-from actiontests import CountAction
+from actiontests import CountAction, FakeEventFilter
# Helper classes to help testing
class SimpleTutorial(Tutorial):
@@ -49,6 +50,28 @@ class SimpleTutorial(Tutorial):
def set_state(self, name):
self.current_state_name = name
+class TutorialTest(unittest.TestCase):
+ """Tests the tutorial functions that are not covered elsewhere."""
+ def test_detach(self):
+ class Activity(object):
+ name = "this"
+
+ activity1 = Activity()
+ activity2 = Activity()
+
+ fsm = FiniteStateMachine("Sample example")
+
+ tutorial = Tutorial("Test tutorial", fsm)
+
+ assert tutorial.activity == None, "There is a default activity in the tutorial"
+
+ tutorial.attach(activity1)
+
+ assert tutorial.activity == activity1, "Activity should have been associated to this tutorial"
+
+ tutorial.attach(activity2)
+ assert tutorial.activity == activity2, "Activity should have been changed to activity2"
+
class TutorialWithFSM(Tutorial):
"""
Fake tutorial, but associated with a FSM.
@@ -73,173 +96,6 @@ class TrueWhileActiveAction(Action):
def undo(self):
self.active = False
-
-class ClickableWidget():
- """
- This class fakes a widget with a clicked() method
- """
- def __init__(self):
- self.click_count = 0
-
- def clicked(self):
- self.click_count += 1
-
-class FakeTextEntry():
- """
- This class fakes a widget with an insert_text() method
- """
- def __init__(self):
- self.text_lines = []
- self.last_entered_line = ""
- self.displayed_text = ""
-
- def insert_text(self, text, index):
- self.last_entered_line = text
- self.text_lines.append(text)
- self.displayed_text = self.displayed_text[0:index] + text + self.displayed_text[index+1:]
-
-class FakeParentWidget():
- """
- This class fakes a widet container, it implements the get_children() method
- """
- def __init__(self):
- self._children = []
-
- def add_child(self, child):
- self._children.append(child)
-
- def get_children(self):
- return self._children
-
-
-
-
-class TriggerEventFilter(EventFilter):
- """
- This event filter can be triggered by simply calling its do_callback function.
-
- Used to fake events and see the effect on the FSM.
- """
- def __init__(self, next_state):
- EventFilter.__init__(self, next_state)
- self.toggle_on_callback = False
-
- def install_handlers(self, callback, **kwargs):
- """
- Forsakes the incoming callback function and just set the inner one.
- """
- self._callback = self._inner_cb
-
- def _inner_cb(self, event_filter):
- self.toggle_on_callback = not self.toggle_on_callback
-
-class FakeEventFilter(TriggerEventFilter):
- """
- This is a fake event that is connected to the tutorial.
-
- The difference between this one and the TriggerEventFilter is that the
- tutorial's set_state will be called on the callback.
-
- Do not forget to add the do_callback() after creating the object.
- """
- def set_tutorial(self, tutorial):
- self.tutorial = tutorial
-
- def _inner_cb(self, event_filter):
- self.toggle_on_callback = not self.toggle_on_callback
- self.tutorial.set_state(event_filter.get_next_state())
-
-
-class ClickActionTests(unittest.TestCase):
- """
- Test class for click action
- """
- def test_do_action(self):
- activity = FakeParentWidget()
- widget = ClickableWidget()
- activity.add_child(widget)
- ObjectStore().activity = activity
-
- action = ClickAction("0.0")
-
- assert widget == ObjectStore().activity.get_children()[0],\
- "The clickable widget isn't reachable from the object store \
- the test cannot pass"
-
- action.do()
-
- assert widget.click_count == 1, "clicked() should have been called by do()"
-
- action.do()
-
- assert widget.click_count == 2, "clicked() should have been called by do()"
-
- def test_undo(self):
- activity = FakeParentWidget()
- widget = ClickableWidget()
- activity.add_child(widget)
- ObjectStore().activity = activity
-
- action = ClickAction("0.0")
-
- assert widget == ObjectStore().activity.get_children()[0],\
- "The clickable widget isn't reachable from the object store \
- the test cannot pass"
-
- action.undo()
-
- #There is no undo for this action so the test should not fail
- assert True
-
-
-
-class TypeTextActionTests(unittest.TestCase):
- """
- Test class for type text action
- """
- def test_do_action(self):
- activity = FakeParentWidget()
- widget = FakeTextEntry()
- activity.add_child(widget)
- ObjectStore().activity = activity
-
- test_text = "This is text"
-
-
- action = TypeTextAction("0.0", test_text)
-
- assert widget == ObjectStore().activity.get_children()[0],\
- "The clickable widget isn't reachable from the object store \
- the test cannot pass"
-
- action.do()
-
- assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
-
- action.do()
-
- assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
- assert len(widget.text_lines) == 2, "insert_text() should have been called twice"
-
- def test_undo(self):
- activity = FakeParentWidget()
- widget = FakeTextEntry()
- activity.add_child(widget)
- ObjectStore().activity = activity
-
- test_text = "This is text"
-
-
- action = TypeTextAction("0.0", test_text)
-
- assert widget == ObjectStore().activity.get_children()[0],\
- "The clickable widget isn't reachable from the object store \
- the test cannot pass"
-
- action.undo()
-
- #There is no undo for this action so the test should not fail
- assert True
# State testing class
class StateTest(unittest.TestCase):
@@ -274,9 +130,9 @@ class StateTest(unittest.TestCase):
Tests the fact that the event filters are correctly installed on setup
and uninstalled on teardown.
"""
- event_filter = TriggerEventFilter("second_state")
+ event_filter = addon.create('TriggerEventFilter')
- state = State("event_test", event_filter_list=[event_filter])
+ state = State("event_test", event_filter_list=[(event_filter, "next_state")])
state.set_tutorial(SimpleTutorial())
assert event_filter.toggle_on_callback == False, "Wrong init of event_filter"
@@ -326,14 +182,11 @@ class StateTest(unittest.TestCase):
assert state.add_action(act2), "Could not add the second action"
assert state.add_action(act3), "Could not add the third action"
- # Try to add a second time an action that was already inserted
- assert state.add_action(act1) == False, "Not supposed to insert an action twice"
-
# Fetch the associated actions
actions = state.get_action_list()
# Make sure all the actions are present in the state
- assert act1 in actions and act2 in actions and act3 in actions,\
+ assert act1 in actions and act2 in actions and act3 in actions, \
"The actions were not properly inserted in the state"
# Clear the list
@@ -345,17 +198,20 @@ class StateTest(unittest.TestCase):
def test_add_event_filter(self):
state = State("INIT")
- event1 = TriggerEventFilter("s")
- event2 = TriggerEventFilter("t")
- event3 = TriggerEventFilter("r")
+ event1 = addon.create('TriggerEventFilter')
+ # MJM : 2009-10-21 : Commenting the below as per new FSM standard, a state cannot
+ # have more than one event filter with the same properties (no identical
+ # properties containers)
+ #event2 = addon.create('TriggerEventFilter')
+ #event3 = addon.create('TriggerEventFilter')
# Insert the event filters
- assert state.add_event_filter(event1), "Could not add event filter 1"
- assert state.add_event_filter(event2), "Could not add event filter 2"
- assert state.add_event_filter(event3), "Could not add event filter 3"
+ assert state.add_event_filter(event1, 's'), "Could not add event filter 1"
+ #assert state.add_event_filter(event2, 't'), "Could not add event filter 2"
+ #assert state.add_event_filter(event3, 'r'), "Could not add event filter 3"
# Make sure we cannot insert an event twice
- assert state.add_event_filter(event1) == False, "Could add twice the event filter"
+ assert state.add_event_filter(event1, 's') == False, "Could add twice the event filter"
# Get the list of event filters
event_filters = state.get_event_filter_list()
@@ -368,7 +224,80 @@ class StateTest(unittest.TestCase):
assert len(state.get_event_filter_list()) == 0, \
"Could not clear the event filter list properly"
+
+ def test_eq_simple(self):
+ """
+ Two empty states with the same name must be identical
+ """
+ st1 = State("Identical")
+ st2 = State("Identical")
+
+ assert st1 == st2, "Empty states with the same name should be identical"
+
+ def test_eq(self):
+ """
+ Test whether two states share the same set of actions and event filters.
+ """
+ st1 = State("Identical")
+ st2 = State("Identical")
+
+ non_state = object()
+
+ act1 = addon.create("BubbleMessage", message="Hi", position=[132,450])
+ act2 = addon.create("BubbleMessage", message="Hi", position=[132,450])
+
+ event1 = addon.create("GtkWidgetEventFilter", "0.0.0.1.1.2.3.1", "clicked")
+
+ act3 = addon.create("DialogMessage", message="Hello again.", position=[200, 400])
+
+ # Build the first state
+ st1.add_action(act1)
+ st1.add_action(act3)
+ st1.add_event_filter(event1, 'nextState')
+
+ # Build the second state
+ st2.add_action(act2)
+ st2.add_action(act3)
+ st2.add_event_filter(event1, 'nextState')
+
+ # Make sure that they are identical for now
+ assert st1 == st2, "States should be considered as identical"
+ assert st2 == st1, "States should be considered as identical"
+ # Modify the second bubble message action
+ act2.message = "New message"
+
+ # Since one action changed in the second state, this should indicate that the states
+ # are not identical anymore
+ assert not (st1 == st2), "Action was changed and states should be different"
+ assert not (st2 == st1), "Action was changed and states should be different"
+
+ # Make sure that trying to find identity with something else than a State object fails properly
+ assert not (st1 == non_state), "Passing a non-State object should fail for identity"
+
+ st2.name = "Not identical anymore"
+ assert not(st1 == st2), "Different state names should give different states"
+ st2.name = "Identical"
+
+ st3 = copy.deepcopy(st1)
+ st3.add_action(addon.create("BubbleMessage", "Hi!", [128,264]))
+
+ assert not (st1 == st3), "States having a different number of actions should be different"
+
+ st4 = copy.deepcopy(st1)
+ st4.add_event_filter(addon.create("GtkWidgetEventFilter", "0.0.1.1.2.2.3", "clicked"), "next_state")
+
+ assert not (st1 == st4), "States having a different number of events should be different"
+
+ st5 = copy.deepcopy(st1)
+ st5.clear_event_filters()
+
+ st5.add_event_filter(addon.create("GtkWidgetEventFilter", "0.1.2.3.4.1.2", "pressed"), "other_state")
+
+ #import rpdb2; rpdb2.start_embedded_debugger('pass')
+ assert not (st1 == st5), "States having the same number of event filters" \
+ + " but those being different should be different"
+
class FSMTest(unittest.TestCase):
"""
This class needs to text the interface and functionality of the Finite
@@ -379,10 +308,10 @@ class FSMTest(unittest.TestCase):
act_init = TrueWhileActiveAction()
act_second = TrueWhileActiveAction()
- event_init = FakeEventFilter("SECOND")
+ event_init = FakeEventFilter()
content = {
- "INIT": State("INIT", action_list=[act_init],event_filter_list=[event_init]),
+ "INIT": State("INIT", action_list=[act_init],event_filter_list=[(event_init, "SECOND")]),
"SECOND": State("SECOND", action_list=[act_second])
}
@@ -413,6 +342,7 @@ class FSMTest(unittest.TestCase):
assert act_second.active == False, "FSM did not teardown SECOND properly"
+
def test_state_insert(self):
"""
This is a simple test to insert, then find a state.
@@ -472,9 +402,9 @@ class FSMTest(unittest.TestCase):
This test removes a state from the FSM. It also verifies that the links
from other states going into the removed state are gone.
"""
- st1 = State("INIT", event_filter_list=[TriggerEventFilter("second")])
- st2 = State("second", event_filter_list=[TriggerEventFilter("third")])
- st3 = State("third", event_filter_list=[TriggerEventFilter("second")])
+ st1 = State("INIT", event_filter_list=[(addon.create('TriggerEventFilter'), "second")])
+ st2 = State("second", event_filter_list=[(addon.create('TriggerEventFilter'), "third")])
+ st3 = State("third", event_filter_list=[(addon.create('TriggerEventFilter'), "second")])
fsm = FiniteStateMachine("StateRemovalTest")
@@ -504,10 +434,10 @@ class FSMTest(unittest.TestCase):
# Make sure that there is no link to the removed state in the rest
# of the FSM
- assert "second" not in fsm.get_following_states("INIT"),\
+ assert "second" not in fsm.get_following_states("INIT"), \
"The link to second from INIT still exists after removal"
- assert "second" not in fsm.get_following_states("third"),\
+ assert "second" not in fsm.get_following_states("third"), \
"The link to second from third still exists after removal"
def test_set_same_state(self):
@@ -534,8 +464,116 @@ class FSMTest(unittest.TestCase):
"The action was triggered a second time, do_count = %d"%do_count
undo_count = fsm.get_state_by_name("INIT").get_action_list()[0].undo_count
- assert fsm.get_state_by_name("INIT").get_action_list()[0].undo_count == 0,\
+ assert fsm.get_state_by_name("INIT").get_action_list()[0].undo_count == 0, \
"The action has been undone unappropriately, undo_count = %d"%undo_count
+
+ def test_setup(self):
+ fsm = FiniteStateMachine("New state machine")
+
+ try:
+ fsm.setup()
+ assert False, "fsm should throw an exception when trying to setup and not bound to a tutorial"
+ except UnboundLocalError:
+ pass
+
+ def test_setup_actions(self):
+ tut = SimpleTutorial()
+
+ states_dict = {"INIT": State("INIT")}
+ fsm = FiniteStateMachine("New FSM", state_dict=states_dict)
+
+ act = CountAction()
+ fsm.add_action(act)
+
+ fsm.set_tutorial(tut)
+
+ fsm.setup()
+
+ # Let's also test the current state name
+ assert fsm.get_current_state_name() == "INIT", "Initial state should be INIT"
+
+ assert act.do_count == 1, "Action should have been called during setup"
+
+ fsm._fsm_has_finished = True
+
+ fsm.teardown()
+
+ assert act.undo_count == 1, "Action should have been undone"
+
+ def test_string_rep(self):
+ fsm = FiniteStateMachine("Testing machine")
+
+ st1 = State("INIT")
+ st2 = State("Other State")
+ st3 = State("Final State")
+
+ st1.add_action(addon.create("BubbleMessage", "Hi!", [132,312]))
+
+ fsm.add_state(st1)
+ fsm.add_state(st2)
+ fsm.add_state(st3)
+
+ assert str(fsm) == "INIT, Final State, Other State, "
+
+ def test_eq_(self):
+ fsm = FiniteStateMachine("Identity test")
+
+ non_fsm_object = object()
+
+ assert not (fsm == non_fsm_object), "Testing with non FSM object should not give identity"
+
+ # Compare FSMs
+ act1 = CountAction()
+
+ fsm.add_action(act1)
+
+ fsm2 = copy.deepcopy(fsm)
+
+ assert fsm == fsm2
+
+ act2 = CountAction()
+ fsm2.add_action(act2)
+
+ assert not(fsm == fsm2), \
+ "FSMs having a different number of actions should be different"
+
+ fsm3 = FiniteStateMachine("Identity test")
+
+ act3 = addon.create("BubbleMessage", "Hi!", [123,312])
+ fsm3.add_action(act3)
+
+ assert not(fsm3 == fsm), \
+ "Actions having the same number of actions but different ones should be different"
+
+ st1 = State("INIT")
+
+ st2 = State("OtherState")
+
+ fsm.add_state(st1)
+ fsm.add_state(st2)
+
+ fsm4 = copy.deepcopy(fsm)
+
+ assert fsm == fsm4
+
+ st3 = State("Last State")
+
+ fsm4.add_state(st3)
+
+ assert not (fsm == fsm4), "FSMs having a different number of states should not be identical"
+
+ fsm4.remove_state("OtherState")
+
+ assert not (fsm == fsm4), "FSMs having different states should be different"
+
+ fsm4.remove_state("Last State")
+
+ st5 = State("OtherState")
+ st5.add_action(CountAction())
+
+ fsm4.add_state(st5)
+
+ assert not(fsm == fsm4), "FSMs having states with same name but different content should be different"
class FSMExplorationTests(unittest.TestCase):
def setUp(self):
@@ -547,13 +585,13 @@ class FSMExplorationTests(unittest.TestCase):
"""
st1 = State("INIT")
st1.add_action(CountAction())
- st1.add_event_filter(TriggerEventFilter("Second"))
- st1.add_event_filter(TriggerEventFilter("Third"))
+ st1.add_event_filter(addon.create('TriggerEventFilter'), "Second")
+ st1.add_event_filter(addon.create('TriggerEventFilter'), "Third")
st2 = State("Second")
st2.add_action(TrueWhileActiveAction())
- st2.add_event_filter(TriggerEventFilter("Third"))
- st2.add_event_filter(TriggerEventFilter("Fourth"))
+ st2.add_event_filter(addon.create('TriggerEventFilter'), "Third")
+ st2.add_event_filter(addon.create('TriggerEventFilter'), "Fourth")
st3 = State("Third")
st3.add_action(CountAction())
@@ -592,6 +630,5 @@ class FSMExplorationTests(unittest.TestCase):
self.validate_previous_states("Fourth", ("Second"))
-
if __name__ == "__main__":
unittest.main()
diff --git a/tests/filterstests.py b/tests/filterstests.py
index 3e79bcc..ee6033b 100644
--- a/tests/filterstests.py
+++ b/tests/filterstests.py
@@ -26,26 +26,16 @@ import time
import gobject
import gtk
-from sugar.tutorius.filters import EventFilter, TimerEvent, GtkWidgetTypeFilter
+from sugar.tutorius.filters import EventFilter
from sugar.tutorius import addon
from gtkutilstests import SignalCatcher
class BaseEventFilterTests(unittest.TestCase):
"""Test the behavior of the Base EventFilter class"""
- def test_properties(self):
- """Test EventFilter properties"""
- e = EventFilter("NEXTSTATE")
-
- assert e.next_state == "NEXTSTATE", "next_state should have value used in constructor"
-
- e.next_state = "NEWSTATE"
-
- assert e.next_state == "NEWSTATE", "next_state should have been changed by setter"
-
def test_callback(self):
"""Test the callback mechanism"""
- e = EventFilter("Next")
+ e = EventFilter()
s = SignalCatcher()
#Trigger the do_callback, shouldn't do anything
@@ -79,7 +69,7 @@ class TestTimerEvent(unittest.TestCase):
ctx = gobject.MainContext()
main = gobject.MainLoop(ctx)
- e = TimerEvent("Next",1) #1 second should be enough :s
+ e = addon.create('TimerEvent', 2) # 2 seconds should be enough :s
s = SignalCatcher()
e.install_handlers(s.callback)
@@ -122,7 +112,7 @@ class TestTimerEvent(unittest.TestCase):
ctx = gobject.MainContext()
main = gobject.MainLoop(ctx)
- e = TimerEvent("Next",1) #1 second should be enough :s
+ e = addon.create('TimerEvent', 2) # 2 seconds should be enough :s
s = SignalCatcher()
e.install_handlers(s.callback)
@@ -169,7 +159,7 @@ class TestGtkWidgetEventFilter(unittest.TestCase):
self.top.add(self.btn1)
def test_install(self):
- h = addon.create('GtkWidgetEventFilter', "Next","0","whatever")
+ h = addon.create('GtkWidgetEventFilter', "0","whatever")
try:
h.install_handlers(None)
@@ -178,7 +168,7 @@ class TestGtkWidgetEventFilter(unittest.TestCase):
assert True, "Install should have failed"
def test_button_clicks(self):
- h = addon.create('GtkWidgetEventFilter', "Next","0.0","clicked")
+ h = addon.create('GtkWidgetEventFilter', "0.0","clicked")
s = SignalCatcher()
h.install_handlers(s.callback, activity=self.top)
diff --git a/tests/linear_creatortests.py b/tests/linear_creatortests.py
index dcded57..8b13656 100644
--- a/tests/linear_creatortests.py
+++ b/tests/linear_creatortests.py
@@ -19,7 +19,7 @@ from sugar.tutorius.core import *
from sugar.tutorius.actions import *
from sugar.tutorius.filters import *
from sugar.tutorius.linear_creator import *
-from coretests import TriggerEventFilter
+from sugar.tutorius.addons.triggereventfilter import *
from actiontests import CountAction
import unittest
@@ -35,11 +35,11 @@ class CreatorTests(unittest.TestCase):
creator.action(CountAction())
creator.action(CountAction())
- creator.event(TriggerEventFilter("Not important"))
+ creator.event(TriggerEventFilter())
creator.action(CountAction())
- creator.event(TriggerEventFilter("Not good either..."))
+ creator.event(TriggerEventFilter())
fsm = creator.generate_fsm()
@@ -50,13 +50,13 @@ class CreatorTests(unittest.TestCase):
assert len(init_state.get_action_list()) == 2, "Creator did not insert all the actions"
- assert init_state.get_event_filter_list()[0].get_next_state() == "State 1" , "expected next state to be 'State 1' but got %s" % init_state.get_event_filter_list()[0].get_next_state()
+ assert init_state.get_event_filter_list()[0][1] == "State 1" , "expected next state to be 'State 1' but got %s" % init_state.get_event_filter_list()[0].get_next_state()
state1 = fsm.get_state_by_name("State 1")
assert len(state1.get_action_list()) == 1, "Creator did not insert all the actions"
- assert state1.get_event_filter_list()[0].get_next_state() == "State 2"
+ assert state1.get_event_filter_list()[0][1] == "State 2"
# Make sure we have the final state and that it's empty
state2 = fsm.get_state_by_name("State2")
diff --git a/tests/probetests.py b/tests/probetests.py
new file mode 100644
index 0000000..a440334
--- /dev/null
+++ b/tests/probetests.py
@@ -0,0 +1,63 @@
+# 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
+"""
+Probe Tests
+
+"""
+
+import unittest
+import os, sys
+import gtk
+import time
+
+from dbus.mainloop.glib import DBusGMainLoop
+import dbus
+
+from sugar.tutorius.TProbe import TProbe, ProbeProxy
+
+
+class FakeActivity(object):
+ def __init__(self):
+ self.top = gtk.Window(type=gtk.WINDOW_TOPLEVEL)
+ self.top.set_name("Top")
+
+ hbox = gtk.HBox()
+ self.top.add(hbox)
+ hbox.show()
+
+ btn1 = gtk.Button()
+ btn1.set_name("Button1")
+ hbox.pack_start(btn1)
+ btn1.show()
+ self.button = btn1
+
+class ProbeTest(unittest.TestCase):
+ def test_ping(self):
+ m = DBusGMainLoop(set_as_default=True)
+ dbus.set_default_main_loop(m)
+
+ activity = FakeActivity()
+ probe = TProbe("localhost.unittest.ProbeTest", activity.top)
+
+ #Parent, ping the probe
+ proxy = ProbeProxy("localhost.unittest.ProbeTest")
+ res = probe.ping()
+
+ assert res == "alive", "Probe should be alive"
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/tests/propertiestests.py b/tests/propertiestests.py
index 46346c4..2494ea6 100644
--- a/tests/propertiestests.py
+++ b/tests/propertiestests.py
@@ -15,6 +15,9 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import unittest
+import uuid
+import os
+import copy
from sugar.tutorius.constraints import *
from sugar.tutorius.properties import *
@@ -81,7 +84,128 @@ class BasePropertyTest(unittest.TestCase):
obj.prop = 2
assert obj.prop == 2, "Unable to set a value on base class"
+
+ def test_eq_(self):
+ class klass(TPropContainer):
+ prop = TutoriusProperty()
+ obj = klass()
+
+ obj2 = klass()
+
+ assert obj == obj2, "Base property containers should be identical"
+class AdvancedPropertyTest(unittest.TestCase):
+ def test_properties_groups(self):
+ """
+ Tests complex properties containers for identity.
+ """
+
+ class klass1(TPropContainer):
+ message = TutoriusProperty()
+ property = TutoriusProperty()
+ data = TutoriusProperty()
+
+ class klass3(TPropContainer):
+ property = TutoriusProperty()
+ message = TutoriusProperty()
+ data = TutoriusProperty()
+ extra_prop = TutoriusProperty()
+
+ class klass4(TPropContainer):
+ property = TutoriusProperty()
+ message = TutoriusProperty()
+ data = TFloatProperty(13.0)
+
+ obj1 = klass1()
+ obj1.property = 12
+ obj1.message = "Initial message"
+ obj1.data = [132, 208, 193, 142]
+
+ obj2 = klass1()
+ obj2.property = 12
+ obj2.message = "Initial message"
+ obj2.data = [132, 208, 193, 142]
+
+ obj3 = klass3()
+ obj3.property = 12
+ obj3.message = "Initial message"
+ obj3.data = [132, 208, 193, 142]
+ obj3.extra_prop = "Suprprise!"
+
+ obj4 = klass4()
+ obj4.property = 12
+ obj4.message = "Initial message"
+ obj4.data = 13.4
+
+ # Ensure that both obj1 and obj2 are identical (they have the same list of
+ # properties and they have the same values
+ assert obj1 == obj1, "Identical objects were considered as different"
+
+ # Ensure that obj1 is different from obj3, since obj3 has an extra property
+ assert not (obj1 == obj3), "Objects should not be identical since obj3 has more props"
+ assert not (obj3 == obj1), "Objects should not be identical since obj3 has more properties"
+
+ # Ensure that properties of different type are considered as different
+ assert not (obj1 == obj4), "Properties of different type should not be equal"
+
+ def test_addon_properties(self):
+ """Test an addon property.
+
+ This tests creates a class with a single addon property (klass1) and
+ assigns a new addon to it (inner1)."""
+ class klass1(TPropContainer):
+ addon = TAddonProperty()
+
+ class inner1(TPropContainer):
+ internal = TutoriusProperty()
+ def __init__(self, value):
+ TPropContainer.__init__(self)
+ self.internal = value
+
+ obj1 = klass1()
+ obj1.addon = inner1("Hi!")
+
+ obj2 = klass1()
+ obj2.addon = inner1("Hi!")
+
+ assert obj1 == obj2, "Identical objects with addon properties were treated as different"
+
+ obj3 = klass1()
+ obj3.addon = inner1("Hello!")
+
+ assert not (obj1 == obj3), "Objects with addon property having a different value should be considered different"
+
+ def test_addonlist_properties(self):
+ class klass1(TPropContainer):
+ addon_list = TAddonListProperty()
+
+ class inner1(TPropContainer):
+ message = TutoriusProperty()
+ data = TutoriusProperty()
+ def __init__(self, message, data):
+ TPropContainer.__init__(self)
+ self.message = message
+ self.data = data
+
+ class inner2(TPropContainer):
+ message = TutoriusProperty()
+ other_data = TutoriusProperty()
+ def __init__(self, message, data):
+ TPropContainer.__init__(self)
+ self.message = message
+ self.other_data = data
+
+ obj1 = klass1()
+ obj1.addon_list = [inner1('Hi!', 12), inner1('Hello.', [1,2])]
+ obj2 = klass1()
+ obj2.addon_list = [inner1('Hi!', 12), inner1('Hello.', [1,2])]
+
+ assert obj1 == obj2, "Addon lists with the same containers were considered different"
+
+ obj3 = klass1()
+ obj3.addon_list = [inner1('Hi!', 12), inner2('Hello.', [1,2])]
+ assert not (obj1 == obj3), "Differently named properties should be considered different in the addon list tests"
+
class TIntPropertyTest(unittest.TestCase):
def test_int_property(self):
class klass(TPropContainer):
@@ -251,7 +375,7 @@ class TArrayPropertyTest(unittest.TestCase):
prop = TArrayProperty([1, 2, 3, 4])
obj = klass()
- assert obj.prop == [1,2,3,4], "Unable to set initial value via constructor"
+ assert obj.prop == (1,2,3,4), "Unable to set initial value via constructor"
assert klass.prop.type == "array", "Wrong type for array : %s"%klass.prop.type
@@ -377,19 +501,38 @@ class TEnumPropertyTest(unittest.TestCase):
try_wrong_values(self.obj)
class TFilePropertyTest(unittest.TestCase):
+ root_folder = "/tmp/tutorius"
+
def setUp(self):
+ try:
+ os.mkdir(self.root_folder)
+ except:
+ pass
+ # Create some sample, unique files for the tests
+ self.temp_filename1 = os.path.join(self.root_folder, "sample_file1_" + str(uuid.uuid1()) + ".txt")
+ self.temp_file1 = file(self.temp_filename1, "w")
+ self.temp_file1.close()
+ self.temp_filename2 = os.path.join(self.root_folder, "sample_file2_" + str(uuid.uuid1()) + ".txt")
+ self.temp_file2 = file(self.temp_filename2, "w")
+ self.temp_file2.close()
+
class klass(TPropContainer):
- prop = TFileProperty("propertiestests.py")
+ prop = TFileProperty(self.temp_filename1)
self.obj = klass()
+
+ def tearDown(self):
+ # Unlink the files from the disk when tests are over
+ os.unlink(self.temp_filename1)
+ os.unlink(self.temp_filename2)
def test_basic_file(self):
- assert self.obj.prop == "propertiestests.py", "Could not set initial value"
+ assert self.obj.prop == self.temp_filename1, "Could not set initial value"
assert type(self.obj).prop.type == "file", "Wrong type for TFileProperty : %s"%type(self.obj).prop.type
- self.obj.prop = "run-tests.py"
+ self.obj.prop = self.temp_filename2
- assert self.obj.prop == "run-tests.py", "Could not change value"
+ assert self.obj.prop == self.temp_filename2, "Could not change value"
try:
self.obj.prop = "unknown/file/on/disk.gif"
@@ -397,6 +540,45 @@ class TFilePropertyTest(unittest.TestCase):
except FileConstraintError:
pass
+class TAddonPropertyTest(unittest.TestCase):
+ def test_wrong_value(self):
+ class klass1(TPropContainer):
+ addon = TAddonProperty()
+
+ class wrongAddon(object):
+ pass
+
+ obj1 = klass1()
+ obj1.addon = klass1()
+
+ try:
+ obj1.addon = wrongAddon()
+ assert False, "Addon Property should not accept non-TPropContainer values"
+ except ValueError:
+ pass
+
+class TAddonPropertyList(unittest.TestCase):
+ def test_wrong_value(self):
+ class klass1(TPropContainer):
+ addonlist = TAddonListProperty()
+
+ class wrongAddon(object):
+ pass
+
+ obj1 = klass1()
+
+ obj1.addonlist = [klass1(), klass1()]
+
+ try:
+ obj1.addonlist = klass1()
+ assert False, "TAddonPropeprty shouldn't accept anything else than a list"
+ except ValueError:
+ pass
+
+ try:
+ obj1.addonlist = [klass1(), klass1(), wrongAddon(), klass1()]
+ except ValueError:
+ pass
if __name__ == "__main__":
unittest.main()
diff --git a/tests/run-tests.py b/tests/run-tests.py
deleted file mode 100755
index d41aa0a..0000000
--- a/tests/run-tests.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/python
-# This is a dumb script to run tests on the sugar-jhbuild installed files
-# The path added is the default path for the jhbuild build
-
-INSTALL_PATH="../../../../../../install/lib/python2.5/site-packages/"
-
-import os, sys
-sys.path.insert(0,
- os.path.abspath(INSTALL_PATH)
-)
-
-FULL_PATH = os.path.join(INSTALL_PATH,"sugar/tutorius")
-SUBDIRS = ["uam"]
-GLOB_PATH = os.path.join(FULL_PATH,"*.py")
-import unittest
-from glob import glob
-def report_files():
- ret = glob(GLOB_PATH)
- for dir in SUBDIRS:
- ret += glob(os.path.join(FULL_PATH,dir,"*.py"))
- return ret
-
-import sys
-if __name__=='__main__':
- if "--coverage" in sys.argv:
- sys.argv=[arg for arg in sys.argv if arg != "--coverage"]
- import coverage
- coverage.erase()
- #coverage.exclude('raise NotImplementedError')
- coverage.start()
-
- import coretests
- import servicestests
- import gtkutilstests
- #import overlaytests # broken
- import linear_creatortests
- import actiontests
- import uamtests
- import filterstests
- import constraintstests
- import propertiestests
- import serializertests
- suite = unittest.TestSuite()
- suite.addTests(unittest.findTestCases(coretests))
- suite.addTests(unittest.findTestCases(servicestests))
- suite.addTests(unittest.findTestCases(gtkutilstests))
- #suite.addTests(unittest.findTestCases(overlaytests)) # broken
- suite.addTests(unittest.findTestCases(linear_creatortests))
- suite.addTests(unittest.findTestCases(actiontests))
- suite.addTests(unittest.findTestCases(uamtests))
- suite.addTests(unittest.findTestCases(filterstests))
- suite.addTests(unittest.findTestCases(constraintstests))
- suite.addTests(unittest.findTestCases(propertiestests))
- suite.addTests(unittest.findTestCases(serializertests))
- runner = unittest.TextTestRunner()
- runner.run(suite)
- coverage.stop()
- coverage.report(report_files())
- coverage.erase()
- else:
- from coretests import *
- from servicestests import *
- from gtkutilstests import *
- #from overlaytests import * # broken
- from actiontests import *
- from linear_creatortests import *
- from uamtests import *
- from filterstests import *
- from constraintstests import *
- from propertiestests import *
- from actiontests import *
- from serializertests import *
-
- unittest.main()
diff --git a/tests/serializertests.py b/tests/serializertests.py
deleted file mode 100644
index 6c25bae..0000000
--- a/tests/serializertests.py
+++ /dev/null
@@ -1,197 +0,0 @@
-# Copyright (C) 2009, Tutorius.org
-# Copyright (C) 2009, Jean-Christophe Savard <savard.jean.christophe@gmail.com>
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-"""
-Serialization Tests
-
-This module contains all the tests that pertain to the usage of the Tutorius
-Serializer object. This means testing saving a tutorial dictionary to a .tml
-file, loading the list of tutorials for this activity and building chosen
-tutorial.
-"""
-
-import unittest
-
-import os
-import shutil
-
-from sugar.tutorius import bundler, addon
-from sugar.tutorius.core import State, FiniteStateMachine
-from sugar.tutorius.actions import *
-from sugar.tutorius.filters import *
-from sugar.tutorius.bundler import XMLSerializer, Serializer
-import sugar
-from uuid import uuid1
-
-class SerializerInterfaceTest(unittest.TestCase):
- """
- For completeness' sake.
- """
- def test_save(self):
- ser = Serializer()
-
- try:
- ser.save_fsm(None)
- assert False, "save_fsm() should throw an unimplemented error"
- except:
- pass
-
- def test_load(self):
- ser = Serializer()
-
- try:
- ser.load_fsm(str(uuid.uuid1()))
- assert False, "load_fsm() should throw an unimplemented error"
- except:
- pass
-
-class XMLSerializerTest(unittest.TestCase):
- """
- Tests the transformation of XML to FSM, then back.
- """
- def setUp(self):
- # Make the serializer believe the test is in a activity path
- self.testpath = "/tmp/testdata/"
- os.environ["SUGAR_BUNDLE_PATH"] = self.testpath
- os.environ["SUGAR_PREFIX"] = self.testpath
- os.environ["SUGAR_PROFILE"] = 'test'
-## os.mkdir(sugar.tutorius.bundler._get_store_root())
-
- # Create the sample FSM
- self.fsm = FiniteStateMachine("testingMachine")
-
- # Add a few states
- act1 = addon.create('BubbleMessage', message="Hi", pos=[300, 450])
- ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked", "Second")
- act2 = addon.create('BubbleMessage', message="Second message", pos=[250, 150], tailpos=[1,2])
-
- st1 = State("INIT")
- st1.add_action(act1)
- st1.add_event_filter(ev1)
-
- st2 = State("Second")
-
- st2.add_action(act2)
-
- self.fsm.add_state(st1)
- self.fsm.add_state(st2)
-
- self.uuid = uuid1()
-
- # Flag to set to True if the output can be deleted after execution of
- # the test
- self.remove = True
-
- def tearDown(self):
- """
- Removes the created files, if need be.
- """
- if self.remove == True:
- shutil.rmtree(os.path.join(os.getenv("HOME"),".sugar",os.getenv("SUGAR_PROFILE")))
- if os.path.isdir(self.testpath):
- shutil.rmtree(self.testpath)
-
- def test_save(self):
- """
- Writes an FSM to disk, then compares the file to the expected results.
- "Remove" boolean argument specify if the test data must be removed or not
- """
- xml_ser = XMLSerializer()
- os.makedirs(os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)))
- #rpdb2.start_embedded_debugger('flakyPass')
- xml_ser.save_fsm(self.fsm, bundler.TUTORIAL_FILENAME, os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)))
-
- def test_save_and_load(self):
- """
- Load up the written FSM and compare it with the object representation.
- """
- self.test_save()
- testpath = "/tmp/testdata/"
- #rpdb2.start_embedded_debugger('flakyPass')
- xml_ser = XMLSerializer()
-
- # This interface needs to be redone... It's not clean because there is
- # a responsibility mixup between the XML reader and the bundler.
- loaded_fsm = xml_ser.load_fsm(str(self.uuid))
-
- # Compare the two FSMs
- assert loaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \
- 'FSM underlying dictionary differ from original to pickled/reformed one'
- assert loaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \
- 'FSM underlying dictionary differ from original to pickled/reformed one'
- assert loaded_fsm._states.get("INIT").get_action_list()[0].message == \
- self.fsm._states.get("INIT").get_action_list()[0].message, \
- 'FSM underlying State underlying Action differ from original to reformed one'
- assert len(loaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself"
-
- def test_all_actions(self):
- """
- Inserts all the known action types in a FSM, then attempt to load it.
- """
- st = State("INIT")
-
- act1 = addon.create('BubbleMessage', "Hi!", pos=[10,120], tailpos=[-12,30])
- act2 = addon.create('DialogMessage', "Hello again.", pos=[120,10])
- act3 = WidgetIdentifyAction()
- act4 = DisableWidgetAction("0.0.0.1.0.0.0")
- act5 = TypeTextAction("0.0.0.1.1.1.0.0", "New text")
- act6 = ClickAction("0.0.1.0.1.1")
- act7 = OnceWrapper(act1)
- act8 = ChainAction([act1, act2, act3, act4])
- actions = [act1, act2, act3, act4, act5, act6, act7, act8]
-
- for action in actions:
- st.add_action(action)
-
- self.fsm.remove_state("Second")
- self.fsm.remove_state("INIT")
- self.fsm.add_state(st)
-
- xml_ser = XMLSerializer()
-
- self.test_save()
-
- reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
- assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading."
-
- def test_all_filters(self):
- """
- Inserts all the known action types in a FSM, then attempt to load it.
- """
- st = State("INIT")
-
- ev1 = TimerEvent("Second", 1000)
- ev2 = addon.create('GtkWidgetEventFilter', "Second", "0.0.1.1.0.0.1", "clicked")
- ev3 = GtkWidgetTypeFilter("Second", "0.0.1.1.1.2.3", text="Typed stuff")
- ev4 = GtkWidgetTypeFilter("Second", "0.0.1.1.1.2.3", strokes="acbd")
- filters = [ev1, ev2, ev3, ev4]
-
- for filter in filters:
- st.add_event_filter(filter)
-
- self.fsm.remove_state("INIT")
- self.fsm.add_state(st)
-
- xml_ser = XMLSerializer()
-
- self.test_save()
-
- reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
-
- assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading."
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/storetests.py b/tests/storetests.py
new file mode 100644
index 0000000..da20c00
--- /dev/null
+++ b/tests/storetests.py
@@ -0,0 +1,107 @@
+# 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 unittest
+
+from sugar.tutorius.store import *
+
+g_tutorial_id = '114db454-b2a1-11de-8cfc-001f5bf747dc'
+g_other_id = '47efc6ee-b2a3-11de-8cfc-001f5bf747dc'
+
+class StoreProxyTest(unittest.TestCase):
+ def setUp(self):
+ self.store = StoreProxy()
+
+ def tearDown(self):
+ pass
+
+ def test_get_categories(self):
+ categories = self.store.get_categories()
+
+ assert isinstance(categories, list), "categories should be a list"
+
+ def test_get_tutorials(self):
+ self.store.get_tutorials()
+
+ def test_get_tutorial_collection(self):
+ collection_list = self.store.get_tutorial_collection('top5_rating')
+
+ assert isinstance(collection_list, list), "get_tutorial_collection should return a list"
+
+ def test_get_latest_version(self):
+ version_dict = self.store.get_latest_version([])
+
+ assert isinstance(version_dict, dict)
+
+ def test_download_tutorial(self):
+ tutorial = self.store.download_tutorial(g_tutorial_id)
+
+ assert tutorial is not None
+
+ def test_login(self):
+ assert self.store.login("unknown_user", "random_password")
+
+ def test_register_new_user(self):
+ user_info = {
+ 'name' : "Albert",
+ 'last_name' : "The Tester",
+ 'location' : 'Mozambique',
+ 'email' : 'albertthetester@mozambique.org'
+ }
+
+ assert self.store.register_new_user(user_info)
+
+
+class StoreProxyLoginTest(unittest.TestCase):
+ def setUp(self):
+ self.store = StoreProxy()
+ self.store.login("unknown_user", "random_password")
+
+ def tearDown(self):
+ session_id = self.store.get_session_id()
+
+ if session_id is not None:
+ self.store.close_session()
+
+ def test_close_session(self):
+ assert self.store.close_session()
+
+ def test_get_session_id(self):
+ session_id = self.store.get_session_id()
+
+ assert session_id is not None
+
+ def test_rate(self):
+ assert self.store.rate(5, g_tutorial_id)
+
+ def test_publish(self):
+ # TODO : We need to send in a real tutorial loaded from
+ # the Vault
+ assert self.store.publish(['This should be a real tutorial...'])
+
+ def test_unpublish(self):
+ # TODO : We need to send in a real tutorial loaded from
+ # the Vault
+ self.store.publish([g_tutorial_id, 'Fake tutorial'])
+
+ assert self.store.unpublish(g_tutorial_id)
+
+ def test_update_published_tutorial(self):
+ # TODO : Run these tests with files from the Vault
+ self.store.publish([g_tutorial_id, 'Fake tutorial'])
+
+ assert self.store.update_published_tutorial(g_tutorial_id, [g_tutorial_id, 'This is an updated tutorial'])
+
diff --git a/tests/vaulttests.py b/tests/vaulttests.py
new file mode 100644
index 0000000..02c34e8
--- /dev/null
+++ b/tests/vaulttests.py
@@ -0,0 +1,516 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Jean-Christophe Savard <savard.jean.christophe@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+Vault Tests
+
+This module contains all the tests that pertain to the usage of the Tutorius
+Vault object. The Vault manage all the interactions with the various Tutorius
+modules dans the local file system. This include saving a tutorial to a .xml
+file, generating the metadata file, finding existing tutorials in the file
+system and building chosen tutorials.
+"""
+
+import unittest
+
+import os
+import shutil
+import zipfile
+
+from sugar.tutorius import addon
+from sugar.tutorius.core import State, FiniteStateMachine, Tutorial
+from sugar.tutorius.actions import *
+from sugar.tutorius.filters import *
+from sugar.tutorius.vault import Vault, XMLSerializer, Serializer, TutorialBundler
+
+import sugar
+
+from uuid import uuid1
+
+class VaultInterfaceTest(unittest.TestCase):
+ """
+ Test the high-level interfaces functions of the Vault
+ """
+
+ def create_test_metadata_file(self, ini_file_path, guid):
+ ini_file = open(ini_file_path, 'wt')
+ ini_file.write("[GENERAL_METADATA]\n")
+ ini_file.write('guid=' + str(guid) + '\n')
+ ini_file.write('name=TestTutorial1\n')
+ ini_file.write('version=1\n')
+ ini_file.write('description=This is a test tutorial 1\n')
+ ini_file.write('rating=3.5\n')
+ ini_file.write('category=Test\n')
+ ini_file.write('publish_state=false\n')
+ ini_file.write('[RELATED_ACTIVITIES]\n')
+ ini_file.write('org.laptop.TutoriusActivity = 1\n')
+ ini_file.write('org.laptop.Writus = 1\n')
+ ini_file.close()
+
+
+ def setUp(self):
+ os.environ["SUGAR_BUNDLE_PATH"] = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path')
+ 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)
+
+ # Generate a first test GUID
+ self.test_guid = uuid1()
+ self.guid_path = os.path.join(sugar.tutorius.vault._get_store_root(),str(self.test_guid))
+ os.mkdir(self.guid_path)
+
+ # Create a first dummy .ini file
+ self.ini_file_path = os.path.join(self.guid_path, "meta.ini")
+ self.create_test_metadata_file(self.ini_file_path, self.test_guid)
+
+ # Generate a second test GUID
+ self.test_guid2 = uuid1()
+ self.guid_path2 = os.path.join(sugar.tutorius.vault._get_store_root(),str(self.test_guid2))
+ os.mkdir(self.guid_path2)
+
+ # Create a second dummy .ini file
+ self.ini_file_path2 = os.path.join(self.guid_path2, "meta.ini")
+
+ ini_file2 = open(self.ini_file_path2, 'wt')
+ ini_file2.write("[GENERAL_METADATA]\n")
+ ini_file2.write('guid=' + str(self.test_guid2) + '\n')
+ ini_file2.write('name=TestTutorial2\n')
+ ini_file2.write('version=2\n')
+ ini_file2.write('description=This is a test tutorial 2\n')
+ ini_file2.write('rating=4\n')
+ ini_file2.write('category=Test2\n')
+ ini_file2.write('publish_state=false\n')
+ ini_file2.write('[RELATED_ACTIVITIES]\n')
+ ini_file2.write('org.laptop.TutoriusActivity = 2\n')
+ ini_file2.write('org.laptop.Writus = 1\n')
+ ini_file2.write('org.laptop.Testus = 1\n')
+ ini_file2.close()
+
+ # Create a dummy fsm
+ self.fsm = FiniteStateMachine("testingMachine")
+ # Add a few states
+ act1 = addon.create('BubbleMessage', message="Hi", position=[300, 450])
+ ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked")
+ act2 = addon.create('BubbleMessage', message="Second message", position=[250, 150], tail_pos=[1,2])
+ st1 = State("INIT")
+ st1.add_action(act1)
+ st1.add_event_filter(ev1, 'Second')
+ st2 = State("Second")
+ st2.add_action(act2)
+ self.fsm.add_state(st1)
+ self.fsm.add_state(st2)
+ self.tuto_guid = uuid1()
+
+ # Create a dummy metadata dictionnary
+ self.test_metadata_dict = {}
+ self.save_test_guid = uuid1()
+ self.test_metadata_dict['name'] = 'TestTutorial1'
+ self.test_metadata_dict['guid'] = str(self.save_test_guid)
+ self.test_metadata_dict['version'] = '1'
+ self.test_metadata_dict['description'] = 'This is a test tutorial 1'
+ self.test_metadata_dict['rating'] = '3.5'
+ self.test_metadata_dict['category'] = 'Test'
+ self.test_metadata_dict['publish_state'] = 'false'
+ activities_dict = {}
+ activities_dict['org.laptop.tutoriusactivity'] = '1'
+ activities_dict['org.laptop,writus'] = '1'
+ self.test_metadata_dict['activities'] = activities_dict
+
+
+ def test_installTutorials(self):
+
+ # TODO : Test for erronous file too (not .xml, not .ini, not .zip, etc.)
+
+ # create dummy tutorial
+
+ # create a test folder in the file
+ # system outside the Vault
+ 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'))
+ os.makedirs(test_path)
+
+ # Creat a dummy tutorial .xml file
+ serializer = XMLSerializer()
+
+ serializer.save_fsm(self.fsm, 'tutorial.xml', test_path)
+
+ # Create a dummy tutorial metadata file
+ self.create_test_metadata_file(os.path.join(test_path, 'meta.ini'), self.tuto_guid)
+
+ #Zip these tutorials files in the pkzip file format
+ archive_list = [os.path.join(test_path, 'meta.ini'), os.path.join(test_path, 'tutorial.xml')]
+
+ zfilename = "TestTutorial.zip"
+
+ zout = zipfile.ZipFile(os.path.join(test_path, zfilename), "w")
+ for fname in archive_list:
+ fname_splitted = fname.rsplit('/')
+ file_only_name = fname_splitted[fname_splitted.__len__() - 1]
+ zout.write(fname, file_only_name)
+ zout.close()
+
+ # test if the file is a valid pkzip file
+ assert zipfile.is_zipfile(os.path.join(test_path, zfilename)) == True, "The zipping of the tutorial files failed."
+
+ # test installTutorials function
+ vault = Vault()
+
+ install_return = vault.installTutorials(test_path, 'TestTutorial.zip', False)
+ assert install_return != 2, "Tutorial install has returned an error"
+
+ # check if the tutorial is now in the vault
+ try :
+ bundler = TutorialBundler(self.tuto_guid)
+ bundler.get_tutorial_path(self.tuto_guid)
+ except IOError:
+ print("Cannot find the specified tutorial's GUID in the vault")
+
+
+ def test_query(self):
+ """
+ Test the query function that return a list of tutorials (dictionnaries) that
+ correspond to the specified parameters.
+ """
+
+ # Note : Temporary only test query that return ALL tutorials in the vault.
+ # TODO : Test with varying parameters
+
+ vault = Vault()
+
+ tutorial_list = vault.query()
+
+ if tutorial_list.__len__() < 2:
+ assert False, 'Error, list doesnt have enough tutorial in it : ' + str(tutorial_list.__len__()) + ' element'
+
+ for tuto_dictionnary in tutorial_list:
+ if tuto_dictionnary['name'] == 'TestTutorial1':
+ related = tuto_dictionnary['activities']
+ assert tuto_dictionnary['version'] == '1'
+ assert tuto_dictionnary['description'] == 'This is a test tutorial 1'
+ assert tuto_dictionnary['rating'] == '3.5'
+ assert tuto_dictionnary['category'] == 'Test'
+ assert tuto_dictionnary['publish_state'] == 'false'
+ assert related.has_key('org.laptop.tutoriusactivity')
+ assert related.has_key('org.laptop.writus')
+
+ elif tuto_dictionnary['name'] == 'TestTutorial2':
+ related = tuto_dictionnary['activities']
+ assert tuto_dictionnary['version'] == '2'
+ assert tuto_dictionnary['description'] == 'This is a test tutorial 2'
+ assert tuto_dictionnary['rating'] == '4'
+ assert tuto_dictionnary['category'] == 'Test2'
+ assert tuto_dictionnary['publish_state'] == 'false'
+ assert related.has_key('org.laptop.tutoriusactivity')
+ assert related.has_key('org.laptop.writus')
+ assert related.has_key('org.laptop.testus')
+
+ else:
+ assert False, 'list is empty or name property is wrong'
+
+
+ def test_loadTutorial(self):
+ """
+ Test the opening of a tutorial from the vault by passing it is guid and
+ returning the Tutorial object representation. This test verify that the
+ initial underlying FSM and the new loaded one are equivalent.
+ """
+
+ # call test_installTutorials to be sure that the tuto is now in the Vault
+ self.test_installTutorials()
+ bundler = TutorialBundler(self.tuto_guid)
+ test = bundler.get_tutorial_path(self.tuto_guid)
+ # load tutorial created in the test_installTutorial function
+ vault = Vault()
+ reloaded_tuto = vault.loadTutorial(self.tuto_guid)
+
+ # Compare the two FSMs
+ reloaded_fsm = reloaded_tuto.state_machine
+
+ assert reloaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \
+ 'FSM underlying dictionary differ from original to reformed one'
+ assert reloaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \
+ 'FSM underlying dictionary differ from original to reformed one'
+ assert reloaded_fsm._states.get("INIT").get_action_list()[0].message == \
+ self.fsm._states.get("INIT").get_action_list()[0].message, \
+ 'FSM underlying State underlying Action differ from original to reformed one'
+ assert len(reloaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself"
+
+ def test_saveTutorial(self):
+ """
+ This test verify the vault function that save a new tutorial (Tutorial object +metadata).
+ """
+
+ # Save the tutorial in the vault
+ vault = Vault()
+ tutorial = Tutorial('test', self.fsm)
+ vault.saveTutorial(tutorial, self.test_metadata_dict)
+
+ # Get the tutorial back
+ reloaded_tuto = vault.loadTutorial(self.save_test_guid)
+
+ # Compare the two FSMs
+ reloaded_fsm = reloaded_tuto.state_machine
+
+ assert reloaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \
+ 'FSM underlying dictionary differ from original to reformed one'
+ assert reloaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \
+ 'FSM underlying dictionary differ from original to reformed one'
+ assert reloaded_fsm._states.get("INIT").get_action_list()[0].message == \
+ self.fsm._states.get("INIT").get_action_list()[0].message, \
+ 'FSM underlying State underlying Action differ from original to reformed one'
+ assert len(reloaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself"
+
+ # TODO : Compare the initial and reloaded metadata when vault.Query() will accept specifc argument
+ # (so we can specifiy that we want only the metadata for this particular tutorial
+
+
+
+ def tearDown(self):
+ folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data');
+ for file in os.listdir(folder):
+ file_path = os.path.join(folder, file)
+ shutil.rmtree(file_path)
+
+ if (os.path.isdir(os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp'))):
+ shutil.rmtree(os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp'))
+
+
+class SerializerInterfaceTest(unittest.TestCase):
+ """
+ For completeness' sake.
+ """
+
+ def test_save(self):
+ ser = Serializer()
+
+ try:
+ ser.save_fsm(None)
+ assert False, "save_fsm() should throw an unimplemented error"
+ except:
+ pass
+
+ def test_load(self):
+ ser = Serializer()
+
+ try:
+ ser.load_fsm(str(uuid.uuid1()))
+ assert False, "load_fsm() should throw an unimplemented error"
+ except:
+ pass
+
+class XMLSerializerTest(unittest.TestCase):
+ """
+ Tests the transformation of XML to FSM, then back.
+ """
+
+ def setUp(self):
+
+ os.environ["SUGAR_BUNDLE_PATH"] = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path')
+ path = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path')
+ if os.path.isdir(path) != True:
+ os.makedirs(path)
+
+ # Create the sample FSM
+ self.fsm = FiniteStateMachine("testingMachine")
+
+ # Add a few states
+ act1 = addon.create('BubbleMessage', message="Hi", position=[300, 450])
+ ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked")
+ act2 = addon.create('BubbleMessage', message="Second message", position=[250, 150], tail_pos=[1,2])
+
+ st1 = State("INIT")
+ st1.add_action(act1)
+ st1.add_event_filter(ev1, 'Second')
+
+ st2 = State("Second")
+
+ st2.add_action(act2)
+
+ self.fsm.add_state(st1)
+ self.fsm.add_state(st2)
+
+ self.uuid = uuid1()
+
+ # Flag to set to True if the output can be deleted after execution of
+ # the test
+ self.remove = True
+
+ def tearDown(self):
+ """
+ Removes the created files, if need be.
+ """
+ if self.remove == True:
+ shutil.rmtree(os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path'))
+
+ folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data');
+ for file in os.listdir(folder):
+ file_path = os.path.join(folder, file)
+ shutil.rmtree(file_path)
+
+ def create_test_metadata(self, ini_file_path, guid):
+ ini_file = open(ini_file_path, 'wt')
+ ini_file.write("[GENERAL_METADATA]\n")
+ ini_file.write('guid=' + str(guid) + '\n')
+ ini_file.write('name=TestTutorial1\n')
+ ini_file.write('version=1\n')
+ ini_file.write('description=This is a test tutorial 1\n')
+ ini_file.write('rating=3.5\n')
+ ini_file.write('category=Test\n')
+ ini_file.write('publish_state=false\n')
+ ini_file.write('[RELATED_ACTIVITIES]\n')
+ ini_file.write('org.laptop.TutoriusActivity = 1\n')
+ ini_file.write('org.laptop.Writus = 1\n')
+ ini_file.close()
+
+ def test_save(self):
+ """
+ Writes an FSM to disk, then compares the file to the expected results.
+ "Remove" boolean argument specify if the test data must be removed or not
+ """
+ xml_ser = XMLSerializer()
+ os.makedirs(os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid)))
+ xml_ser.save_fsm(self.fsm, sugar.tutorius.vault.TUTORIAL_FILENAME, os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid)))
+ self.create_test_metadata(os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid), 'meta.ini'), self.uuid)
+
+
+ def test_save_and_load(self):
+ """
+ Load up the written FSM and compare it with the object representation.
+ """
+ self.test_save()
+ xml_ser = XMLSerializer()
+
+ loaded_fsm = xml_ser.load_fsm(str(self.uuid))
+
+ # Compare the two FSMs
+ assert loaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \
+ 'FSM underlying dictionary differ from original to reformed one'
+ assert loaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \
+ 'FSM underlying dictionary differ from original to reformed one'
+ assert loaded_fsm._states.get("INIT").get_action_list()[0].message == \
+ self.fsm._states.get("INIT").get_action_list()[0].message, \
+ 'FSM underlying State underlying Action differ from original to reformed one'
+ assert len(loaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself"
+
+ def test_all_actions(self):
+ """
+ Inserts all the known action types in a FSM, then attempt to load it.
+ """
+ st = State("INIT")
+
+ act1 = addon.create('BubbleMessage', "Hi!", position=[10,120], tail_pos=[-12,30])
+ act2 = addon.create('DialogMessage', "Hello again.", position=[120,10])
+ act3 = addon.create('WidgetIdentifyAction')
+ act4 = addon.create('DisableWidgetAction', "0.0.0.1.0.0.0")
+ act5 = addon.create('TypeTextAction', "0.0.0.1.1.1.0.0", "New text")
+ act6 = addon.create('ClickAction', "0.0.1.0.1.1")
+ act7 = addon.create('OnceWrapper', action=act1)
+ act8 = addon.create('ChainAction', actions=[act1, act2, act3, act4])
+ actions = [act1, act2, act3, act4, act5, act6, act7, act8]
+
+ for action in actions:
+ st.add_action(action)
+
+ self.fsm.remove_state("Second")
+ self.fsm.remove_state("INIT")
+ self.fsm.add_state(st)
+
+ xml_ser = XMLSerializer()
+
+ self.test_save()
+
+ reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
+
+ # TODO : Cannot do object equivalence, must check equality of all underlying object
+ # assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading."
+
+ def test_all_filters(self):
+ """
+ Inserts all the known action filters in a FSM, then attempt to load it.
+ """
+ st = State("INIT")
+
+ ev1 = addon.create('TimerEvent', 1000)
+ ev2 = addon.create('GtkWidgetEventFilter', object_id="0.0.1.1.0.0.1", event_name="clicked")
+ ev3 = addon.create('GtkWidgetTypeFilter', "0.0.1.1.1.2.3", text="Typed stuff")
+ ev4 = addon.create('GtkWidgetTypeFilter', "0.0.1.1.1.2.3", strokes="acbd")
+ filters = [ev1, ev2, ev3, ev4]
+
+ for filter in filters:
+ st.add_event_filter(filter, 'Second')
+
+ self.fsm.remove_state("INIT")
+ self.fsm.add_state(st)
+
+ xml_ser = XMLSerializer()
+
+ self.test_save()
+
+ reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
+
+ # TODO : Cannot do object equivalence, must check equality of all underlying object
+ # assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading."
+
+
+class TutorialBundlerTests(unittest.TestCase):
+ """
+ TutorialBundler tests
+
+ This module contains all the tests for the storage mecanisms for tutorials
+ This mean testing saving and loading tutorial, .ini file management and
+ adding ressources to tutorial
+ """
+
+ def setUp(self):
+ os.environ["SUGAR_BUNDLE_PATH"] = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path')
+ path = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path')
+ if os.path.isdir(path) != True:
+ os.makedirs(path)
+
+ #generate a test GUID
+ self.test_guid = uuid1()
+ self.guid_path = os.path.join(sugar.tutorius.vault._get_store_root(),str(self.test_guid))
+ os.mkdir(self.guid_path)
+
+ self.ini_file = os.path.join(self.guid_path, "meta.ini")
+
+ ini_file = open(self.ini_file, 'wt')
+ ini_file.write('[GENERAL_METADATA]\n')
+ ini_file.write('guid=' + str(self.test_guid) + '\n')
+ ini_file.write('name=TestTutorial\n')
+ ini_file.write('version=1\n')
+ ini_file.write('description=This is a test tutorial\n')
+ ini_file.write('rating=3.5\n')
+ ini_file.write('category=Test\n')
+ ini_file.write('publish_state=false\n')
+ ini_file.write('[RELATED_ACTIVITES]\n')
+ ini_file.write('org.laptop.TutoriusActivity = 1\n')
+ ini_file.write('org.laptop.Writus = 1\n')
+ ini_file.close()
+
+ def tearDown(self):
+ os.remove(self.ini_file)
+ os.rmdir(self.guid_path)
+
+ folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data');
+ for file in os.listdir(folder):
+ file_path = os.path.join(folder, file)
+ shutil.rmtree(file_path)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
new file mode 100644
index 0000000..6d7b6e2
--- /dev/null
+++ b/tutorius/TProbe.py
@@ -0,0 +1,506 @@
+import logging
+LOGGER = logging.getLogger("sugar.tutorius.TProbe")
+import os
+
+import gobject
+
+import dbus
+import dbus.service
+import cPickle as pickle
+
+import sugar.tutorius.addon as addon
+
+from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.properties import TPropContainer
+
+from sugar.tutorius.dbustools import remote_call, save_args
+import copy
+
+"""
+ --------------------
+ | ProbeManager |
+ --------------------
+ |
+ V
+ -------------------- ----------
+ | ProbeProxy |<---- DBus ---->| TProbe |
+ -------------------- ----------
+
+"""
+#TODO Add stub error handling for remote calls in the classes so that it will
+# be clearer how errors can be handled in the future.
+
+
+class TProbe(dbus.service.Object):
+ """ Tutorius Probe
+ Defines an entry point for Tutorius into activities that allows
+ performing actions and registering events onto an activity via
+ a DBUS Interface.
+ """
+
+ def __init__(self, activity_name, activity):
+ """
+ Create and register a TProbe for an activity.
+
+ @param activity_name unique activity_id
+ @param activity activity reference, must be a gtk container
+ """
+ LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid())
+ LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default()))
+ LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth()))
+ # Moving the ObjectStore assignment here, in the meantime
+ # the reference to the activity shouldn't be share as a
+ # global variable but passed by the Probe to the objects
+ # that requires it
+ self._activity = activity
+
+ ObjectStore().activity = activity
+
+ self._activity_name = activity_name
+ self._session_bus = dbus.SessionBus()
+
+ # Giving a new name because _name is already used by dbus
+ self._name2 = dbus.service.BusName(activity_name, self._session_bus)
+ dbus.service.Object.__init__(self, self._session_bus, "/tutorius/Probe")
+
+ # Add the dictionary we will use to store which actions and events
+ # are known
+ self._installedActions = {}
+ self._subscribedEvents = {}
+
+ def start(self):
+ """
+ Optional method to call if the probe is not inserted into an
+ existing activity. Starts a gobject mainloop
+ """
+ mainloop = gobject.MainLoop()
+ print "Starting Probe for " + self._activity_name
+ mainloop.run()
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def registered(self, service):
+ print ("Registered with: " + str(service))
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='', out_signature='s')
+ def ping(self):
+ """
+ Allows testing the connection to a Probe
+ @return string "alive"
+ """
+ return "alive"
+
+ # ------------------ Action handling --------------------------------------
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='s')
+ def install(self, pickled_action):
+ """
+ Install an action on the Activity
+ @param pickled_action string pickled action
+ @return string address of installed action
+ """
+ loaded_action = pickle.loads(str(pickled_action))
+ action = addon.create(loaded_action.__class__.__name__)
+
+ address = self._generate_action_reference(action)
+
+ self._installedActions[address] = action
+
+ if action._props:
+ action._props.update(loaded_action._props)
+
+ action.do()
+
+ return address
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='ss', out_signature='')
+ def update(self, address, action_props):
+ """
+ Update an already registered action
+ @param address string address returned by install()
+ @param action_props pickled action properties
+ @return None
+ """
+ action = self._installedActions[address]
+
+ if action._props:
+ props = pickle.loads(str(action_props))
+ action._props.update(props)
+ action.undo()
+ action.do()
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def uninstall(self, address):
+ """
+ Uninstall an action
+ @param address string address returned by install()
+ @return None
+ """
+ if self._installedActions.has_key(address):
+ action = self._installedActions[address]
+ action.undo()
+ self._installedActions.pop(address)
+
+
+ # ------------------ Event handling ---------------------------------------
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='s')
+ def subscribe(self, pickled_event):
+ """
+ Subscribe to an Event
+ @param pickled_event string pickled EventFilter
+ @return string unique name of registered event
+ """
+ #TODO Perform event unmapping once Tutorials use abstract events
+ # instead of concrete EventFilters that are tied to their
+ # implementation.
+ eventfilter = pickle.loads(str(pickled_event))
+
+ # The callback uses the event defined previously and each
+ # successive call to subscribe will register a different
+ # callback that references a different event
+ def callback(*args):
+ self.notify(eventfilter)
+
+ eventfilter.install_handlers(callback, activity=self._activity)
+
+ name = self._generate_event_reference(eventfilter)
+ self._subscribedEvents[name] = eventfilter
+
+ return name
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def unsubscribe(self, address):
+ """
+ Remove subscription to an event
+ @param address string adress returned by subscribe()
+ @return None
+ """
+
+ if self._subscribedEvents.has_key(address):
+ eventfilter = self._subscribedEvents[address]
+ eventfilter.remove_handlers()
+ self._subscribedEvents.pop(address)
+
+ @dbus.service.signal("org.tutorius.ProbeInterface")
+ def eventOccured(self, event):
+ # We need no processing now, the signal will be sent
+ # when the method exit
+ pass
+
+ # The actual method we will call on the probe to send events
+ def notify(self, event):
+ LOGGER.debug("TProbe :: notify event %s", str(event))
+ self.eventOccured(pickle.dumps(event))
+
+ # Return a unique name for this action
+ def _generate_action_reference(self, action):
+ # TODO elavoie 2009-07-25 Should return a universal address
+ name = action.__class__.__name__
+ suffix = 1
+
+ while self._installedActions.has_key(name+str(suffix)):
+ suffix += 1
+
+ return name + str(suffix)
+
+
+ # Return a unique name for this event
+ def _generate_event_reference(self, event):
+ # TODO elavoie 2009-07-25 Should return a universal address
+ name = event.__class__.__name__
+ #Keep the counter to avoid looping all the time
+ suffix = getattr(self, '_event_ref_suffix', 0 ) + 1
+
+ while self._subscribedEvents.has_key(name+str(suffix)):
+ suffix += 1
+
+ #setattr(self, '_event_ref_suffix', suffix)
+
+ return name + str(suffix)
+
+class ProbeProxy:
+ """
+ ProbeProxy is a Proxy class for connecting to a remote TProbe.
+
+ It provides an object interface to the TProbe, which requires pickled
+ strings, across a DBus communication.
+ """
+ def __init__(self, activityName):
+ """
+ Constructor
+ @param activityName unique activity id. Must be a valid dbus bus name.
+ """
+ LOGGER.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid())
+ LOGGER.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default()))
+ LOGGER.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth()))
+ bus = dbus.SessionBus()
+ self._object = bus.get_object(activityName, "/tutorius/Probe")
+ self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface")
+
+ self._actions = {}
+ # We keep those two data structures to be able to have multiple callbacks
+ # for the same event and be able to remove them independently
+ # _subscribedEvents holds a list of callback addresses's for each event
+ # _registeredCallbacks holds the functions to call for each address
+ self._subscribedEvents = {}
+ self._registeredCallbacks = {}
+
+
+ self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface")
+
+ def _handle_signal(self, pickled_event):
+ event = pickle.loads(str(pickled_event))
+ LOGGER.debug("ProbeProxy :: Received Event : %s %s", str(event), str(event._props.items()))
+
+ LOGGER.debug("ProbeProxy :: Currently %d events registered", len(self._registeredCallbacks))
+ if self._registeredCallbacks.has_key(event):
+ for callback in self._registeredCallbacks[event].values():
+ callback(event)
+ else:
+ for event in self._registeredCallbacks.keys():
+ LOGGER.debug("==== %s", str(event._props.items()))
+ LOGGER.debug("ProbeProxy :: Event does not appear to be registered")
+
+ def isAlive(self):
+ try:
+ return self._probe.ping() == "alive"
+ except:
+ return False
+
+ def __update_action(self, action, address):
+ LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address))
+ self._actions[action] = str(address)
+
+ def __clear_action(self, action):
+ self._actions.pop(action, None)
+
+ def install(self, action, block=False):
+ """
+ Install an action on the TProbe's activity
+ @param action Action to install
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ return remote_call(self._probe.install, (pickle.dumps(action),),
+ save_args(self.__update_action, action),
+ block=block)
+
+ def update(self, action, newaction, block=False):
+ """
+ Update an already installed action's properties and run it again
+ @param action Action to update
+ @param newaction Action to update it with
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ #TODO review how to make this work well
+ if not action in self._actions:
+ raise RuntimeWarning("Action not installed")
+ #TODO Check error handling
+ return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block)
+
+ def uninstall(self, action, block=False):
+ """
+ Uninstall an installed action
+ @param action Action to uninstall
+ @param block Force a synchroneous dbus call if True
+ """
+ if action in self._actions:
+ remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block)
+
+ def __update_event(self, event, callback, address):
+ LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address))
+ # Since multiple callbacks could be associated to the same
+ # event signature, we will store multiple callbacks
+ # in a dictionary indexed by the unique address
+ # given for this subscribtion and access this
+ # dictionary from another one indexed by event
+ address = str(address)
+
+ # We use the event object as a key
+ if not self._registeredCallbacks.has_key(event):
+ self._registeredCallbacks[event] = {}
+
+ # TODO elavoie 2009-07-25 decide on a proper exception
+ # taxonomy
+ if self._registeredCallbacks[event].has_key(address):
+ # Oups, how come we have two similar addresses?
+ # send the bad news!
+ raise Exception("Probe subscribe exception, the following address already exists: " + str(address))
+
+ self._registeredCallbacks[event][address] = callback
+
+ # We will keep another dictionary to remember the
+ # event that was associated to this unique address
+ # Let's copy to make sure that even if the event
+ # passed in is modified later it won't screw up
+ # our dictionary (python pass arguments by reference)
+ self._subscribedEvents[address] = copy.copy(event)
+
+ return address
+
+ def __clear_event(self, address):
+ LOGGER.debug("ProbeProxy :: Unregistering adress %s", str(address))
+ # Cleanup everything
+ if self._subscribedEvents.has_key(address):
+ event = self._subscribedEvents[address]
+
+ if self._registeredCallbacks.has_key(event)\
+ and self._registeredCallbacks[event].has_key(address):
+ self._registeredCallbacks[event].pop(address)
+
+ if self._registeredCallbacks[event] == {}:
+ self._registeredCallbacks.pop(event)
+
+ self._subscribedEvents.pop(address)
+ else:
+ LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address)
+
+ def subscribe(self, event, callback, block=True):
+ """
+ Register an event listener
+ @param event Event to listen for
+ @param callback callable that will be called when the event occurs
+ @param block Force a synchroneous dbus call if True (Not allowed yet)
+ @return address identifier used for unsubscribing
+ """
+ LOGGER.debug("ProbeProxy :: Registering event %s", str(hash(event)))
+ if not block:
+ raise RuntimeError("This function does not allow non-blocking mode yet")
+
+ # TODO elavoie 2009-07-25 When we will allow for patterns both
+ # for event types and sources, we will need to revise the lookup
+ # mecanism for which callback function to call
+ return remote_call(self._probe.subscribe, (pickle.dumps(event),),
+ save_args(self.__update_event, event, callback),
+ block=block)
+
+ def unsubscribe(self, address, block=True):
+ """
+ Unregister an event listener
+ @param address identifier given by subscribe()
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address))
+ if not block:
+ raise RuntimeError("This function does not allow non-blocking mode yet")
+ if address in self._subscribedEvents.keys():
+ remote_call(self._probe.unsubscribe, (address,),
+ return_cb=save_args(self.__clear_event, address),
+ block=block)
+ else:
+ LOGGER.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address)
+
+ def detach(self, block=False):
+ """
+ Detach the ProbeProxy from it's TProbe. All installed actions and
+ subscribed events should be removed.
+ """
+ for action in self._actions.keys():
+ self.uninstall(action, block)
+
+ for address in self._subscribedEvents.keys():
+ self.unsubscribe(address, block)
+
+
+class ProbeManager(object):
+ """
+ The ProbeManager provides multiplexing across multiple activity ProbeProxies
+
+ For now, it only handles one at a time, though.
+ Actually it doesn't do much at all. But it keeps your encapsulation happy
+ """
+ def __init__(self):
+ self._probes = {}
+ self._current_activity = None
+
+ def setCurrentActivity(self, activity_id):
+ if not activity_id in self._probes:
+ raise RuntimeError("Activity not attached")
+ self._current_activity = activity_id
+
+ def getCurrentActivity(self):
+ return self._current_activity
+
+ currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
+ def attach(self, activity_id):
+ if activity_id in self._probes:
+ raise RuntimeWarning("Activity already attached")
+
+ self._probes[activity_id] = ProbeProxy(activity_id)
+ #TODO what do we do with this? Raise something?
+ if self._probes[activity_id].isAlive():
+ print "Alive!"
+ else:
+ print "FAil!"
+
+ def detach(self, activity_id):
+ if activity_id in self._probes:
+ probe = self._probes.pop(activity_id)
+ probe.detach()
+
+ def install(self, action, block=False):
+ """
+ Install an action on the current activity
+ @param action Action to install
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].install(action, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def update(self, action, newaction, block=False):
+ """
+ Update an already installed action's properties and run it again
+ @param action Action to update
+ @param newaction Action to update it with
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].update(action, newaction, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def uninstall(self, action, block=False):
+ """
+ Uninstall an installed action
+ @param action Action to uninstall
+ @param block Force a synchroneous dbus call if True
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].uninstall(action, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def subscribe(self, event, callback):
+ """
+ Register an event listener
+ @param event Event to listen for
+ @param callback callable that will be called when the event occurs
+ @return address identifier used for unsubscribing
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].subscribe(event, callback)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def unsubscribe(self, address):
+ """
+ Unregister an event listener
+ @param address identifier given by subscribe()
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].unsubscribe(address)
+ else:
+ raise RuntimeWarning("No activity attached")
+
diff --git a/tutorius/actions.py b/tutorius/actions.py
index 4269cd7..08f55cd 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -16,16 +16,14 @@
"""
This module defines Actions that can be done and undone on a state
"""
+import gtk
+
from gettext import gettext as _
-from sugar.tutorius import gtkutils, addon
-from dialog import TutoriusDialog
-import overlayer
-from sugar.tutorius.editor import WidgetIdentifier
+from sugar.tutorius import addon
from sugar.tutorius.services import ObjectStore
from sugar.tutorius.properties import *
from sugar.graphics import icon
-import gtk.gdk
class DragWrapper(object):
"""Wrapper to allow gtk widgets to be dragged around"""
@@ -176,149 +174,4 @@ class Action(TPropContainer):
x, y = self._drag.position
self.position = [int(x), int(y)]
self.__edit_img.destroy()
-
-class OnceWrapper(Action):
- """
- Wraps a class to perform an action once only
-
- This ConcreteActions's do() method will only be called on the first do()
- and the undo() will be callable after do() has been called
- """
-
- _action = TAddonProperty()
-
- def __init__(self, action):
- Action.__init__(self)
- self._called = False
- self._need_undo = False
- self._action = action
-
- def do(self):
- """
- Do the action only on the first time
- """
- if not self._called:
- self._called = True
- self._action.do()
- self._need_undo = True
-
- def undo(self):
- """
- Undo the action if it's been done
- """
- if self._need_undo:
- self._action.undo()
- self._need_undo = False
-
-class WidgetIdentifyAction(Action):
- def __init__(self):
- Action.__init__(self)
- self.activity = None
- self._dialog = None
-
- def do(self):
- os = ObjectStore()
- if os.activity:
- self.activity = os.activity
-
- self._dialog = WidgetIdentifier(self.activity)
- self._dialog.show()
-
-
- def undo(self):
- if self._dialog:
- self._dialog.destroy()
-
-class ChainAction(Action):
- """Utility class to allow executing actions in a specific order"""
- def __init__(self, *actions):
- """ChainAction(action1, ... ) builds a chain of actions"""
- Action.__init__(self)
- self._actions = actions
-
- def do(self,**kwargs):
- """do() each action in the chain"""
- for act in self._actions:
- act.do(**kwargs)
-
- def undo(self):
- """undo() each action in the chain, starting with the last"""
- for act in reversed(self._actions):
- act.undo()
-
-class DisableWidgetAction(Action):
- def __init__(self, target):
- """Constructor
- @param target target treeish
- """
- Action.__init__(self)
- self._target = target
- self._widget = None
-
- def do(self):
- """Action do"""
- os = ObjectStore()
- if os.activity:
- self._widget = gtkutils.find_widget(os.activity, self._target)
- if self._widget:
- self._widget.set_sensitive(False)
-
- def undo(self):
- """Action undo"""
- if self._widget:
- self._widget.set_sensitive(True)
-
-
-class TypeTextAction(Action):
- """
- Simulate a user typing text in a widget
- Work on any widget that implements a insert_text method
-
- @param widget The treehish representation of the widget
- @param text the text that is typed
- """
- def __init__(self, widget, text):
- Action.__init__(self)
-
- self._widget = widget
- self._text = text
-
- def do(self, **kwargs):
- """
- Type the text
- """
- widget = gtkutils.find_widget(ObjectStore().activity, self._widget)
- if hasattr(widget, "insert_text"):
- widget.insert_text(self._text, -1)
-
- def undo(self):
- """
- no undo
- """
- pass
-
-class ClickAction(Action):
- """
- Action that simulate a click on a widget
- Work on any widget that implements a clicked() method
-
- @param widget The threehish representation of the widget
- """
- def __init__(self, widget):
- Action.__init__(self)
- self._widget = widget
-
- def do(self):
- """
- click the widget
- """
- widget = gtkutils.find_widget(ObjectStore().activity, self._widget)
- if hasattr(widget, "clicked"):
- widget.clicked()
- def undo(self):
- """
- No undo
- """
- pass
-
diff --git a/tutorius/addon.py b/tutorius/addon.py
index 51791d1..7ac68f7 100644
--- a/tutorius/addon.py
+++ b/tutorius/addon.py
@@ -38,6 +38,9 @@ import logging
PREFIX = __name__+"s"
PATH = re.sub("addon\\.py[c]$", "", __file__)+"addons"
+TYPE_ACTION = 'action'
+TYPE_EVENT = 'event'
+
_cache = None
def _reload_addons():
@@ -47,16 +50,23 @@ def _reload_addons():
mod = __import__(PREFIX+'.'+re.sub("\\.py$", "", addon), {}, {}, [""])
if hasattr(mod, "__action__"):
_cache[mod.__action__['name']] = mod.__action__
+ mod.__action__['type'] = TYPE_ACTION
continue
if hasattr(mod, "__event__"):
_cache[mod.__event__['name']] = mod.__event__
+ mod.__event__['type'] = TYPE_EVENT
def create(name, *args, **kwargs):
global _cache
if not _cache:
_reload_addons()
try:
- return _cache[name]['class'](*args, **kwargs)
+ comp_metadata = _cache[name]
+ try:
+ return comp_metadata['class'](*args, **kwargs)
+ except:
+ logging.error("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs)))
+ return None
except KeyError:
logging.error("Addon not found for class '%s'", name)
return None
@@ -73,4 +83,13 @@ def get_addon_meta(name):
_reload_addons()
return _cache[name]
+def get_name_from_type(typ):
+ global _cache
+ if not _cache:
+ _reload_addons()
+ for addon in _cache.keys():
+ if typ == _cache[addon]['class']:
+ return addon
+ return None
+
# vim:set ts=4 sts=4 sw=4 et:
diff --git a/tutorius/bundler.py b/tutorius/bundler.py
deleted file mode 100644
index 8808d93..0000000
--- a/tutorius/bundler.py
+++ /dev/null
@@ -1,558 +0,0 @@
-# Copyright (C) 2009, Tutorius.org
-# Copyright (C) 2009, Jean-Christophe Savard <savard.jean.christophe@gmail.com>
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-
-
-"""
-This module contains all the data handling class of Tutorius
-"""
-
-import logging
-import os
-import uuid
-import xml.dom.minidom
-
-from sugar.tutorius import addon
-from sugar.tutorius.core import Tutorial, State, FiniteStateMachine
-from sugar.tutorius.filters import *
-from sugar.tutorius.actions import *
-from ConfigParser import SafeConfigParser
-
-# this is where user installed/generated tutorials will go
-def _get_store_root():
- profile_name = os.getenv("SUGAR_PROFILE") or "default"
- return os.path.join(os.getenv("HOME"),
- ".sugar",profile_name,"tutorius","data")
-# this is where activity bundled tutorials should be, under the activity bundle
-def _get_bundle_root():
- return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data")
-
-INI_ACTIVITY_SECTION = "RELATED_ACTIVITIES"
-INI_METADATA_SECTION = "GENERAL_METADATA"
-INI_GUID_PROPERTY = "GUID"
-INI_NAME_PROPERTY = "NAME"
-INI_XML_FSM_PROPERTY = "FSM_FILENAME"
-INI_FILENAME = "meta.ini"
-TUTORIAL_FILENAME = "tutorial.xml"
-NODE_COMPONENT = "Component"
-
-class TutorialStore(object):
-
- def list_available_tutorials(self, activity_name, activity_vers):
- """
- Generate the list of all tutorials present on disk for a
- given activity.
-
- @returns a map of tutorial {names : GUID}.
- """
- # check both under the activity data and user installed folders
- paths = [_get_store_root(), _get_bundle_root()]
-
- tutoGuidName = {}
-
- for repository in paths:
- # (our) convention dictates that tutorial folders are named
- # with their GUID (for unicity) but this is not enforced.
- try:
- for tuto in os.listdir(repository):
- parser = SafeConfigParser()
- parser.read(os.path.join(repository, tuto, INI_FILENAME))
- guid = parser.get(INI_METADATA_SECTION, INI_GUID_PROPERTY)
- name = parser.get(INI_METADATA_SECTION, INI_NAME_PROPERTY)
- activities = parser.options(INI_ACTIVITY_SECTION)
- # enforce matching activity name AND version, as UI changes
- # break tutorials. We may lower this requirement when the
- # UAM gets less dependent on the widget order.
- # Also note property names are always stored lowercase.
- if activity_name.lower() in activities:
- version = parser.get(INI_ACTIVITY_SECTION, activity_name)
- if activity_vers == version:
- tutoGuidName[guid] = name
- except OSError:
- # the repository may not exist. Continue scanning
- pass
-
- return tutoGuidName
-
- def load_tutorial(self, Guid):
- """
- Rebuilds a tutorial object from it's serialized state.
- Common storing paths will be scanned.
-
- @param Guid the generic identifier of the tutorial
- @returns a Tutorial object containing an FSM
- """
- bundle = TutorialBundler(Guid)
- bundle_path = bundle.get_tutorial_path()
- config = SafeConfigParser()
- config.read(os.path.join(bundle_path, INI_FILENAME))
-
- serializer = XMLSerializer()
-
- name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY)
- fsm = serializer.load_fsm(Guid)
-
- tuto = Tutorial(name, fsm)
- return tuto
-
-
-class Serializer(object):
- """
- Interface that provide serializing and deserializing of the FSM
- used in the tutorials to/from disk. Must be inherited.
- """
-
- def save_fsm(self,fsm):
- """
- Save fsm to disk. If a GUID parameter is provided, the existing GUID is
- located in the .ini files in the store root and bundle root and
- the corresponding FSM is/are overwritten. If the GUId is not found, an
- exception occur. If no GUID is provided, FSM is written in a new file
- in the store root.
- """
- NotImplementedError
-
- def load_fsm(self):
- """
- Load fsm from disk.
- """
- NotImplementedError
-
-class XMLSerializer(Serializer):
- """
- Class that provide serializing and deserializing of the FSM
- used in the tutorials to/from a .xml file. Inherit from Serializer
- """
-
- def _create_state_dict_node(self, state_dict, doc):
- """
- Create and return a xml Node from a State dictionnary.
- """
- statesList = doc.createElement("States")
- for state_name, state in state_dict.items():
- stateNode = doc.createElement("State")
- statesList.appendChild(stateNode)
- stateNode.setAttribute("Name", state_name)
- actionsList = stateNode.appendChild(self._create_action_list_node(state.get_action_list(), doc))
- eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc))
- return statesList
-
- def _create_component_node(self, comp, doc):
- """
- Takes a single component (action or eventfilter) and transforms it
- into a xml node.
-
- @param comp A single component
- @param doc The XML document root (used to create nodes only
- @return A XML Node object with the component tag name
- """
- compNode = doc.createElement(NODE_COMPONENT)
-
- # Write down just the name of the Action class as the Class
- # property --
- compNode.setAttribute("Class",type(comp).__name__)
-
- # serialize all tutorius properties
- for propname in comp.get_properties():
- propval = getattr(comp, propname)
- if getattr(type(comp), propname).type == "addonlist":
- for subval in propval:
- compNode.appendChild(self._create_component_node(subval, doc))
- elif getattr(type(comp), propname).type == "addonlist":
- compNode.appendChild(self._create_component_node(subval, doc))
- else:
- # repr instead of str, as we want to be able to eval() it into a
- # valid object.
- compNode.setAttribute(propname, repr(propval))
-
- return compNode
-
- def _create_action_list_node(self, action_list, doc):
- """
- Create and return a xml Node from a Action list.
-
- @param action_list A list of actions
- @param doc The XML document root (used to create new nodes only)
- @return A XML Node object with the Actions tag name and a serie of
- Action children
- """
- actionsList = doc.createElement("Actions")
- for action in action_list:
- # Create the action node
- actionNode = self._create_component_node(action, doc)
- # Append it to the list
- actionsList.appendChild(actionNode)
-
- return actionsList
-
- def _create_event_filters_node(self, event_filters, doc):
- """
- Create and return a xml Node from a event filters.
- """
- eventFiltersList = doc.createElement("EventFiltersList")
- for event_f in event_filters:
- eventFilterNode = self._create_component_node(event_f, doc)
- eventFiltersList.appendChild(eventFilterNode)
-
- return eventFiltersList
-
- def save_fsm(self, fsm, xml_filename, path):
- """
- Save fsm to disk, in the xml file specified by "xml_filename", in the
- "path" folder. If the specified file doesn't exist, it will be created.
- """
- self.doc = doc = xml.dom.minidom.Document()
- fsm_element = doc.createElement("FSM")
- doc.appendChild(fsm_element)
- fsm_element.setAttribute("Name", fsm.name)
- fsm_element.setAttribute("StartStateName", fsm.start_state_name)
- statesDict = fsm_element.appendChild(self._create_state_dict_node(fsm._states, doc))
-
- fsm_actions_node = self._create_action_list_node(fsm.actions, doc)
- fsm_actions_node.tagName = "FSMActions"
- actionsList = fsm_element.appendChild(fsm_actions_node)
-
- file_object = open(os.path.join(path, xml_filename), "w")
- file_object.write(doc.toprettyxml())
- file_object.close()
-
-
- def _find_tutorial_dir_with_guid(self, guid):
- """
- Finds the tutorial with the associated GUID. If it is found, return
- the path to the tutorial's directory. If it doesn't exist, raise an
- IOError.
-
- A note : if there are two tutorials with this GUID in the folders,
- they will both be inspected and the one with the highest version
- number will be returned. If they have the same version number, the one
- from the global store will be returned.
-
- @param guid The GUID of the tutorial that is to be loaded.
- """
- # Attempt to find the tutorial's directory in the global directory
- global_dir = os.path.join(_get_store_root(), guid)
- # Then in the activty's bundle path
- activity_dir = os.path.join(_get_bundle_root(), guid)
-
- # If they both exist
- if os.path.isdir(global_dir) and os.path.isdir(activity_dir):
- # Inspect both metadata files
- global_meta = os.path.join(global_dir, "meta.ini")
- activity_meta = os.path.join(activity_dir, "meta.ini")
-
- # Open both config files
- global_parser = SafeConfigParser()
- global_parser.read(global_meta)
-
- activity_parser = SafeConfigParser()
- activity_parser.read(activity_meta)
-
- # Get the version number for each tutorial
- global_version = global_parser.get(INI_METADATA_SECTION, "version")
- activity_version = activity_parser.get(INI_METADATA_SECTION, "version")
-
- # If the global version is higher or equal, we'll take it
- if global_version >= activity_version:
- return global_dir
- else:
- return activity_dir
-
- # Do we just have the global directory?
- if os.path.isdir(global_dir):
- return global_dir
-
- # Or just the activity's bundle directory?
- if os.path.isdir(activity_dir):
- return activity_dir
-
- # Error : none of these directories contain the tutorial
- raise IOError(2, "Neither the global nor the bundle directory contained the tutorial with GUID %s"%guid)
-
- def _load_xml_properties(self, properties_elem):
- """
- Changes a list of properties into fully instanciated properties.
-
- @param properties_elem An XML element reprensenting a list of
- properties
- """
- return []
-
- def _load_xml_event_filters(self, filters_elem):
- """
- Loads up a list of Event Filters.
-
- @param filters_elem An XML Element representing a list of event filters
- """
- reformed_event_filters_list = []
- event_filter_element_list = filters_elem.getElementsByTagName(NODE_COMPONENT)
- new_event_filter = None
-
- for event_filter in event_filter_element_list:
- new_event_filter = self._load_xml_component(event_filter)
-
- if new_event_filter is not None:
- reformed_event_filters_list.append(new_event_filter)
-
- return reformed_event_filters_list
-
- def _load_xml_component(self, node):
- """
- Loads a single addon component instance from an Xml node.
-
- @param node The component XML Node to transform
- object
- @return The addon component object of the correct type according to the XML
- description
- """
- new_action = addon.create(node.getAttribute("Class"))
- if not new_action:
- return None
-
- for attrib in node.attributes.keys():
- if attrib == "Class": continue
- # security note: keep sandboxed
- setattr(new_action, attrib, eval(node.getAttribute(attrib), {}, {}))
-
- # recreate complex attributes
- for sub in node.childNodes:
- name = getattr(new_action, sub.nodeName)
- if name == "addon":
- setattr(new_action, sub.getAttribute("Name"), self._load_xml_action(sub))
-
- return new_action
-
- def _load_xml_actions(self, actions_elem):
- """
- Transforms an Actions element into a list of instanciated Action.
-
- @param actions_elem An XML Element representing a list of Actions
- """
- reformed_actions_list = []
- actions_element_list = actions_elem.getElementsByTagName(NODE_COMPONENT)
-
- for action in actions_element_list:
- new_action = self._load_xml_component(action)
-
- reformed_actions_list.append(new_action)
-
- return reformed_actions_list
-
- def _load_xml_states(self, states_elem):
- """
- Takes in a States element and fleshes out a complete list of State
- objects.
-
- @param states_elem An XML Element that represents a list of States
- """
- reformed_state_list = []
- # item(0) because there is always only one <States> tag in the xml file
- # so states_elem should always contain only one element
- states_element_list = states_elem.item(0).getElementsByTagName("State")
-
- for state in states_element_list:
- stateName = state.getAttribute("Name")
- # Using item 0 in the list because there is always only one
- # Actions and EventFilterList element per State node.
- actions_list = self._load_xml_actions(state.getElementsByTagName("Actions")[0])
- event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList")[0])
- reformed_state_list.append(State(stateName, actions_list, event_filters_list))
-
- return reformed_state_list
-
- def _load_xml_fsm(self, fsm_elem):
- """
- Takes in an XML element representing an FSM and returns the fully
- crafted FSM.
-
- @param fsm_elem The XML element that describes a FSM
- """
- # Load the FSM's name and start state's name
- fsm_name = fsm_elem.getAttribute("Name")
-
- fsm_start_state_name = None
- try:
- fsm_start_state_name = fsm_elem.getAttribute("StartStateName")
- except:
- pass
-
- fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name)
-
- # Load the states
- states = self._load_xml_states(fsm_elem.getElementsByTagName("States"))
- for state in states:
- fsm.add_state(state)
-
- # Load the actions on this FSM
- actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions")[0])
- for action in actions:
- fsm.add_action(action)
-
- # Load the event filters
- events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0])
- for event in events:
- fsm.add_event_filter(event)
-
- return fsm
-
-
- def load_fsm(self, guid):
- """
- Load fsm from xml file whose .ini file guid match argument guid.
- """
- # Fetch the directory (if any)
- tutorial_dir = self._find_tutorial_dir_with_guid(guid)
-
- # Open the XML file
- tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME)
-
- xml_dom = xml.dom.minidom.parse(tutorial_file)
-
- fsm_elem = xml_dom.getElementsByTagName("FSM")[0]
-
- return self._load_xml_fsm(fsm_elem)
-
-
-class TutorialBundler(object):
- """
- This class provide the various data handling methods useable by the tutorial
- editor.
- """
-
- def __init__(self,generated_guid = None):
- """
- Tutorial_bundler constructor. If a GUID is given in the parameter, the
- Tutorial_bundler object will be associated with it. If no GUID is given,
- a new GUID will be generated,
- """
-
- self.Guid = generated_guid or str(uuid.uuid1())
-
- #Look for the file in the path if a uid is supplied
- if generated_guid:
- #General store
- store_path = os.path.join(_get_store_root(), generated_guid, INI_FILENAME)
- if os.path.isfile(store_path):
- self.Path = os.path.dirname(store_path)
- else:
- #Bundle store
- bundle_path = os.path.join(_get_bundle_root(), generated_guid, INI_FILENAME)
- if os.path.isfile(bundle_path):
- self.Path = os.path.dirname(bundle_path)
- else:
- raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid)
-
- else:
- #Create the folder, any failure will go through to the caller for now
- store_path = os.path.join(_get_store_root(), self.Guid)
- os.makedirs(store_path)
- self.Path = store_path
-
- def write_metadata_file(self, tutorial):
- """
- Write metadata to the property file.
- @param tutorial Tutorial for which to write metadata
- """
- #Create the Config Object and populate it
- cfg = SafeConfigParser()
- cfg.add_section(INI_METADATA_SECTION)
- cfg.set(INI_METADATA_SECTION, INI_GUID_PROPERTY, self.Guid)
- cfg.set(INI_METADATA_SECTION, INI_NAME_PROPERTY, tutorial.name)
- cfg.set(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY, TUTORIAL_FILENAME)
- cfg.add_section(INI_ACTIVITY_SECTION)
- cfg.set(INI_ACTIVITY_SECTION, os.environ['SUGAR_BUNDLE_NAME'],
- os.environ['SUGAR_BUNDLE_VERSION'])
-
- #Write the ini file
- cfg.write( file( os.path.join(self.Path, INI_FILENAME), 'w' ) )
-
- def get_tutorial_path(self):
- """
- Return the path of the .ini file associated with the guiven guid set in
- the Guid property of the Tutorial_Bundler. If the guid is present in
- more than one path, the store_root is given priority.
- """
-
- store_root = _get_store_root()
- bundle_root = _get_bundle_root()
-
- config = SafeConfigParser()
- path = None
-
- logging.debug("************ Path of store_root folder of activity : " \
- + store_root)
-
- # iterate in each GUID subfolder
- for dir in os.listdir(store_root):
-
- # iterate for each .ini file in the store_root folder
-
- for file_name in os.listdir(os.path.join(store_root, dir)):
- if file_name.endswith(".ini"):
- logging.debug("******************* Found .ini file : " \
- + file_name)
- config.read(os.path.join(store_root, dir, file_name))
- if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == self.Guid:
- xml_filename = config.get(INI_METADATA_SECTION,
- INI_XML_FSM_PROPERTY)
-
- path = os.path.join(store_root, dir)
- return path
-
- logging.debug("************ Path of bundle_root folder of activity : " \
- + bundle_root)
-
-
- # iterate in each GUID subfolder
- for dir in os.listdir(bundle_root):
-
- # iterate for each .ini file in the bundle_root folder
- for file_name in os.listdir(os.path.join(bundle_root, dir)):
- if file_name.endswith(".ini"):
- logging.debug("******************* Found .ini file : " \
- + file_name)
- config.read(os.path.join(bundle_root, dir, file_name))
- if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == self.Guid:
- path = os.path.join(bundle_root, self.Guid)
- return path
-
- if path is None:
- logging.debug("**************** Error : GUID not found")
- raise KeyError
-
- def write_fsm(self, fsm):
-
- """
- Save fsm to disk. If a GUID parameter is provided, the existing GUID is
- located in the .ini files in the store root and bundle root and
- the corresponding FSM is/are created or overwritten. If the GUID is not
- found, an exception occur.
- """
-
- config = SafeConfigParser()
-
- serializer = XMLSerializer()
- path = os.path.join(self.Path, "meta.ini")
- config.read(path)
- xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY)
- serializer.save_fsm(fsm, xml_filename, self.Path)
-
-
- def add_resources(self, typename, file):
- """
- Add ressources to metadata.
- """
- raise NotImplementedError("add_resources not implemented")
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 36abdfb..e91f23a 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -200,7 +200,10 @@ class FileConstraint(Constraint):
def validate(self, value):
# TODO : Decide on the architecture for file retrieval on disk
# Relative paths? From where? Support macros?
- #
+ # FIXME This is a hack to make cases where a default file is not valid
+ # work. It allows None values to be validated, though
+ if value is None:
+ return
if not os.path.isfile(value):
raise FileConstraintError("Non-existing file : %s"%value)
return
diff --git a/tutorius/core.py b/tutorius/core.py
index dd2435e..d08c136 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -21,14 +21,12 @@ This module contains the core classes for tutorius
"""
-import gtk
import logging
-import copy
import os
-from sugar.tutorius.dialog import TutoriusDialog
-from sugar.tutorius.gtkutils import find_widget
-from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.TProbe import ProbeManager
+from sugar.tutorius.dbustools import save_args
+from sugar.tutorius import addon
logger = logging.getLogger("tutorius")
@@ -36,8 +34,11 @@ class Tutorial (object):
"""
Tutorial Class, used to run through the FSM.
"""
+ #Properties
+ probeManager = property(lambda self: self._probeMgr)
+ activityId = property(lambda self: self._activity_id)
- def __init__(self, name, fsm,filename= None):
+ def __init__(self, name, fsm, filename=None):
"""
Creates an unattached tutorial.
"""
@@ -51,21 +52,22 @@ class Tutorial (object):
self.state = None
self.handlers = []
- self.activity = None
+ self._probeMgr = ProbeManager()
+ self._activity_id = None
#Rest of initialisation happens when attached
- def attach(self, activity):
+ def attach(self, activity_id):
"""
Attach to a running activity
- @param activity the activity to attach to
+ @param activity_id the id of the activity to attach to
"""
#For now, absolutely detach if a previous one!
- if self.activity:
+ if self._activity_id:
self.detach()
- self.activity = activity
- ObjectStore().activity = activity
- ObjectStore().tutorial = self
+ self._activity_id = activity_id
+ self._probeMgr.attach(activity_id)
+ self._probeMgr.currentActivity = activity_id
self._prepare_activity()
self.state_machine.set_state("INIT")
@@ -77,9 +79,9 @@ class Tutorial (object):
# Uninstall the whole FSM
self.state_machine.teardown()
- #FIXME There should be some amount of resetting done here...
- self.activity = None
-
+ if not self._activity_id is None:
+ self._probeMgr.detach(self._activity_id)
+ self._activity_id = None
def set_state(self, name):
"""
@@ -89,18 +91,6 @@ class Tutorial (object):
self.state_machine.set_state(name)
-
- # Currently unused -- equivalent function is in each state
- def _eventfilter_state_done(self, eventfilter):
- """
- Callback handler for eventfilter to notify
- when we must go to the next state.
- """
- #XXX Tests should be run here normally
-
- #Swith to the next state pointed by the eventfilter
- self.set_state(eventfilter.get_next_state())
-
def _prepare_activity(self):
"""
Prepare the activity for the tutorial by loading the saved state and
@@ -112,9 +102,11 @@ class Tutorial (object):
#of the activity root directory
filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\
self.activity_init_state_filename
- if os.path.exists(filename):
- self.activity.read_file(filename)
-
+ readfile = addon.create("ReadFile", filename=filename)
+ if readfile:
+ self._probeMgr.install(readfile)
+ #Uninstall now while we have the reference handy
+ self._probeMgr.uninstall(readfile)
class State(object):
"""
@@ -141,10 +133,9 @@ class State(object):
self._actions = action_list or []
- # Unused for now
- #self.tests = []
+ self._transitions= dict(event_filter_list or [])
- self._event_filters = event_filter_list or []
+ self._installedEvents = set()
self.tutorial = tutorial
@@ -168,12 +159,11 @@ class State(object):
Install the state itself, by first registering the event filters
and then triggering the actions.
"""
- for eventfilter in self._event_filters:
- eventfilter.install_handlers(self._event_filter_state_done_cb,
- activity=self.tutorial.activity)
+ for (event, next_state) in self._transitions.items():
+ self._installedEvents.add(self.tutorial.probeManager.subscribe(event, save_args(self._event_filter_state_done_cb, next_state )))
for action in self._actions:
- action.do()
+ self.tutorial.probeManager.install(action)
def teardown(self):
"""
@@ -182,38 +172,37 @@ class State(object):
removing dialogs that were displayed, removing highlights, etc...
"""
# Remove the handlers for the all of the state's event filters
- for event_filter in self._event_filters:
- event_filter.remove_handlers()
+ while len(self._installedEvents) > 0:
+ self.tutorial.probeManager.unsubscribe(self._installedEvents.pop())
# Undo all the actions related to this state
for action in self._actions:
- action.undo()
+ self.tutorial.probeManager.uninstall(action)
- def _event_filter_state_done_cb(self, event_filter):
+ def _event_filter_state_done_cb(self, next_state, event):
"""
Callback for event filters. This function needs to inform the
tutorial that the state is over and tell it what is the next state.
- @param event_filter The event filter that was called
+ @param next_state The next state for the transition
+ @param event The event that occured
"""
# Run the tests here, if need be
# Warn the higher level that we wish to change state
- self.tutorial.set_state(event_filter.get_next_state())
+ self.tutorial.set_state(next_state)
# Model manipulation
# These functions are used to simplify the creation of states
def add_action(self, new_action):
"""
- Adds an action to the state (only if it wasn't added before)
+ Adds an action to the state
@param new_action The new action to execute when in this state
@return True if added, False otherwise
"""
- if new_action not in self._actions:
- self._actions.append(new_action)
- return True
- return False
+ self._actions.append(new_action)
+ return True
# remove_action - We did not define names for the action, hence they're
# pretty hard to remove on a precise basis
@@ -229,19 +218,21 @@ class State(object):
Removes all the action associated with this state. A cleared state will
not do anything when entered or exited.
"""
+ #FIXME What if the action is currently installed?
self._actions = []
- def add_event_filter(self, event_filter):
+ def add_event_filter(self, event, next_state):
"""
Adds an event filter that will cause a transition from this state.
The same event filter may not be added twice.
- @param event_filter The new event filter that will trigger a transition
+ @param event The event that will trigger a transition
+ @param next_state The state to which the transition will lead
@return True if added, False otherwise
"""
- if event_filter not in self._event_filters:
- self._event_filters.append(event_filter)
+ if event not in self._transitions.keys():
+ self._transitions[event]=next_state
return True
return False
@@ -249,7 +240,7 @@ class State(object):
"""
@return The list of event filters associated with this state.
"""
- return self._event_filters
+ return self._transitions.items()
def clear_event_filters(self):
"""
@@ -257,7 +248,63 @@ class State(object):
was just cleared will become a sink and will be the end of the
tutorial.
"""
- self._event_filters = []
+ self._transitions = {}
+
+ def __eq__(self, otherState):
+ """
+ Compares two states and tells whether they contain the same states with the
+ same actions and event filters.
+
+ @param otherState The other State that we wish to match
+ @returns True if every action in this state has a matching action in the
+ other state with the same properties and values AND if every
+ event filters in this state has a matching filter in the
+ other state having the same properties and values AND if both
+ states have the same name.
+` """
+ if not isinstance(otherState, State):
+ return False
+ if self.name != otherState.name:
+ return False
+
+ # Do they have the same actions?
+ if len(self._actions) != len(otherState._actions):
+ return False
+
+ if len(self._transitions) != len(otherState._transitions):
+ return False
+
+ for act in self._actions:
+ found = False
+ # For each action in the other state, try to match it with this one.
+ for otherAct in otherState._actions:
+ if act == otherAct:
+ found = True
+ break
+ if found == False:
+ # If we arrive here, then we could not find an action with the
+ # same values in the other state. We know they're not identical
+ return False
+
+ # Do they have the same event filters?
+ for event in self._transitions:
+ state_name = self._transitions[event]
+ found = False
+ # For every event filter in the other state, try to match it with
+ # the current filter. We just need to find one with the right
+ # properties and values.
+ for otherEvent in otherState._transitions:
+ other_state_name = otherState._transitions[otherEvent]
+ if event == otherEvent:
+ found = True
+ break
+ if found == False:
+ # We could not find the given event filter in the other state.
+ return False
+
+ # If nothing failed up to now, then every actions and every filters can
+ # be found in the other state
+ return True
class FiniteStateMachine(State):
"""
@@ -349,7 +396,7 @@ class FiniteStateMachine(State):
self._fsm_setup_done = True
# Execute all the FSM level actions
for action in self.actions:
- action.do()
+ self.tutorial.probeManager.install(action)
# Then, we need to run the setup of the current state
self.current_state.setup()
@@ -414,7 +461,7 @@ class FiniteStateMachine(State):
self._fsm_teardown_done = True
# Undo all the FSM level actions here
for action in self.actions:
- action.undo()
+ self.tutorial.probeManager.uninstall(action)
# TODO : It might be nice to have a start() and stop() method for the
# FSM.
@@ -470,9 +517,10 @@ class FiniteStateMachine(State):
#TODO : Move this code inside the State itself - we're breaking
# encap :P
- for event_filter in st._event_filters:
- if event_filter.get_next_state() == state_name:
- st._event_filters.remove(event_filter)
+ for event in st._transitions.keys():
+ state = st._transitions[event]
+ if state == state_name:
+ del st._transitions[event]
# Remove the state from the dictionary
del self._states[state_name]
@@ -490,8 +538,9 @@ class FiniteStateMachine(State):
next_states = set()
- for event_filter in state._event_filters:
- next_states.add(event_filter.get_next_state())
+ for event in state._transitions.keys():
+ state_name_in_dict = state._transitions[event]
+ next_states.add(state_name_in_dict)
return tuple(next_states)
@@ -513,9 +562,9 @@ class FiniteStateMachine(State):
states = []
# Walk through the list of states
for st in self._states.itervalues():
- for event_filter in st._event_filters:
- if event_filter.get_next_state() == state_name:
- states.append(event_filter.get_next_state())
+ for event, state in st._transitions.items():
+ if state == state_name:
+ states.append(state)
continue
return tuple(states)
@@ -526,3 +575,58 @@ class FiniteStateMachine(State):
for st in self._states.itervalues():
out_string += st.name + ", "
return out_string
+
+ def __eq__(self, otherFSM):
+ """
+ Compares the elements of two FSM to ensure and returns true if they have the
+ same set of states, containing the same actions and the same event filters.
+
+ @returns True if the two FSMs have the same content, False otherwise
+ """
+ if not isinstance(otherFSM, FiniteStateMachine):
+ return False
+
+ # Make sure they share the same name
+ if not (self.name == otherFSM.name) or \
+ not (self.start_state_name == otherFSM.start_state_name):
+ return False
+
+ # Ensure they have the same number of FSM-level actions
+ if len(self._actions) != len(otherFSM._actions):
+ return False
+
+ # Test that we have all the same FSM level actions
+ for act in self._actions:
+ found = False
+ # For every action in the other FSM, try to match it with the
+ # current one.
+ for otherAct in otherFSM._actions:
+ if act == otherAct:
+ found = True
+ break
+ if found == False:
+ return False
+
+ # Make sure we have the same number of states in both FSMs
+ if len(self._states) != len(otherFSM._states):
+ return False
+
+ # For each state, try to find a corresponding state in the other FSM
+ for state_name in self._states.keys():
+ state = self._states[state_name]
+ other_state = None
+ try:
+ # Attempt to use this key in the other FSM. If it's not present
+ # the dictionary will throw an exception and we'll know we have
+ # at least one different state in the other FSM
+ other_state = otherFSM._states[state_name]
+ except:
+ return False
+ # If two states with the same name exist, then we want to make sure
+ # they are also identical
+ if not state == other_state:
+ return False
+
+ # If we made it here, then all the states in this FSM could be matched to an
+ # identical state in the other FSM.
+ return True
diff --git a/tutorius/creator.py b/tutorius/creator.py
index 7455ecb..d5595e1 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -22,16 +22,20 @@ the activity itself.
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk.gdk
+import gtk.glade
import gobject
from gettext import gettext as T
-from sugar.graphics.toolbutton import ToolButton
+import os
+from sugar.graphics import icon
+import copy
from sugar.tutorius import overlayer, gtkutils, actions, bundler, properties, addon
-from sugar.tutorius import filters
+from sugar.tutorius import filters, __path__
from sugar.tutorius.services import ObjectStore
from sugar.tutorius.linear_creator import LinearCreator
-from sugar.tutorius.core import Tutorial
+from sugar.tutorius.core import Tutorial, FiniteStateMachine, State
+from sugar.tutorius import viewer
class Creator(object):
"""
@@ -47,80 +51,171 @@ class Creator(object):
"""
self._activity = activity
if not tutorial:
- self._tutorial = LinearCreator()
+ self._tutorial = FiniteStateMachine('Untitled')
+ self._state = State(name='INIT')
+ self._tutorial.add_state(self._state)
+ self._state_counter = 1
else:
self._tutorial = tutorial
+ # TODO load existing tutorial; unused yet
self._action_panel = None
self._current_filter = None
self._intro_mask = None
self._intro_handle = None
- self._state_bubble = overlayer.TextBubble(self._tutorial.state_name)
allocation = self._activity.get_allocation()
self._width = allocation.width
self._height = allocation.height
self._selected_widget = None
self._eventmenu = None
+ self.tuto = None
+ self._guid = None
self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5))
self._activity._overlayer.put(self._hlmask, 0, 0)
- self._activity._overlayer.put(self._state_bubble,
- self._width/2-self._state_bubble.allocation.width/2, 0)
-
dlg_width = 300
dlg_height = 70
sw = gtk.gdk.screen_width()
sh = gtk.gdk.screen_height()
- self._tooldialog = gtk.Window()
- self._tooldialog.set_title("Tutorius tools")
- self._tooldialog.set_transient_for(self._activity)
- self._tooldialog.set_decorated(True)
- self._tooldialog.set_resizable(False)
- self._tooldialog.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY)
- self._tooldialog.set_destroy_with_parent(True)
- self._tooldialog.set_deletable(False)
- self._tooldialog.set_size_request(dlg_width, dlg_height)
-
- toolbar = gtk.Toolbar()
- for tool in addon.list_addons():
- meta = addon.get_addon_meta(tool)
- toolitem = ToolButton(meta['icon'])
- toolitem.set_tooltip(meta['display_name'])
- toolitem.connect("clicked", self._add_action_cb, tool)
- toolbar.insert(toolitem, -1)
- toolitem = ToolButton("go-next")
- toolitem.connect("clicked", self._add_step_cb)
- toolitem.set_tooltip("Add Step")
- toolbar.insert(toolitem, -1)
- toolitem = ToolButton("stop")
- toolitem.connect("clicked", self._cleanup_cb)
- toolitem.set_tooltip("End Tutorial")
- toolbar.insert(toolitem, -1)
- self._tooldialog.add(toolbar)
- self._tooldialog.show_all()
- # simpoir: I suspect the realized widget is a tiny bit larger than
- # it should be, thus the -10.
- self._tooldialog.move(sw-10-dlg_width, sh-dlg_height)
-
- self._propedit = EditToolBox(self._activity)
-
- def _evfilt_cb(self, menuitem, event_name, *args):
+
+ self._propedit = ToolBox(self._activity)
+ self._propedit.tree.signal_autoconnect({
+ 'on_quit_clicked': self._cleanup_cb,
+ 'on_save_clicked': self.save,
+ 'on_action_activate': self._add_action_cb,
+ 'on_event_activate': self._add_event_cb,
+ })
+ self._propedit.window.move(
+ gtk.gdk.screen_width()-self._propedit.window.get_allocation().width,
+ 100)
+
+
+ self._overview = viewer.Viewer(self._tutorial, self)
+ self._overview.win.set_transient_for(self._activity)
+
+ self._overview.win.move(0, gtk.gdk.screen_height()- \
+ self._overview.win.get_allocation().height)
+
+ self._transitions = dict()
+
+ def set_next_state(self, state, event, next_state):
+ # FIXME HACK
+ self._transitions[event] = next_state
+
+ evts = state.get_event_filter_list()
+ state.clear_event_filters()
+ for evt, next_state in evts:
+ state.add_event_filter(evt, self._transitions[evt])
+
+ def delete_action(self, action):
+ """
+ Removes the first instance of specified action from the tutorial.
+
+ @param action: the action object to remove from the tutorial
+ @returns: True if successful, otherwise False.
+ """
+ state = self._tutorial.get_state_by_name("INIT")
+
+ while True:
+ state_actions = state.get_action_list()
+ for fsm_action in state_actions:
+ if fsm_action is action:
+ state.clear_actions()
+ if state is self._state:
+ fsm_action.exit_editmode()
+ state_actions.remove(fsm_action)
+ self.set_insertion_point(state.name)
+ for keep_action in state_actions:
+ state.add_action(keep_action)
+ return True
+
+ ev_list = state.get_event_filter_list()
+ if ev_list:
+ state = self._tutorial.get_state_by_name(ev_list[0][1])
+ #ev_list[0].get_next_state())
+ continue
+
+ return False
+
+ def delete_state(self):
+ """
+ Remove current state.
+ Limitation: The last state cannot be removed, as it doesn't have
+ any transitions to remove anyway.
+
+ @returns: True if successful, otherwise False.
+ """
+ if not self._state.get_event_filter_list():
+ # last state cannot be removed
+ return False
+
+ state = self._tutorial.get_state_by_name("INIT")
+ ev_list = state.get_event_filter_list()
+ if state is self._state:
+ next_state = self._tutorial.get_state_by_name(ev_list[0][1])
+ #ev_list[0].get_next_state())
+ self.set_insertion_point(next_state.name)
+ self._tutorial.remove_state(state.name)
+ self._tutorial.remove_state(next_state.name)
+ next_state.name = "INIT"
+ self._tutorial.add_state(next_state)
+ return True
+
+ # loop to repair links from deleted state
+ while ev_list:
+ next_state = self._tutorial.get_state_by_name(ev_list[0][1])
+ #ev_list[0].get_next_state())
+ if next_state is self._state:
+ # the tutorial will flush the event filters. We'll need to
+ # clear and re-add them.
+ self._tutorial.remove_state(self._state.name)
+ state.clear_event_filters()
+ # FIXME HACK START
+ self.set_next_state(state, ev_list[0][0], next_state.get_event_filter_list()[0][1])
+ #ev_list[0].set_next_state(
+ # next_state.get_event_filter_list()[0].get_next_state())
+ # FIXME HACK END
+ for ev, next_state in ev_list:
+ state.add_event_filter(ev, next_state)
+
+ self.set_insertion_point(ev_list[0][1])
+ #self.set_insertion_point(ev_list[0].get_next_state())
+ return True
+
+ state = next_state
+ ev_list = state.get_event_filter_list()
+ return False
+
+ def get_insertion_point(self):
+ return self._state.name
+
+ def set_insertion_point(self, state_name):
+ for action in self._state.get_action_list():
+ action.exit_editmode()
+ self._state = self._tutorial.get_state_by_name(state_name)
+ self._overview.win.queue_draw()
+ state_actions = self._state.get_action_list()
+ for action in state_actions:
+ action.enter_editmode()
+ action._drag._eventbox.connect_after(
+ "button-release-event", self._action_refresh_cb, action)
+
+ if state_actions:
+ self._propedit.action = state_actions[0]
+ else:
+ self._propedit.action = None
+
+
+ def _evfilt_cb(self, menuitem, event):
"""
This will get called once the user has selected a menu item from the
event filter popup menu. This should add the correct event filter
to the FSM and increment states.
"""
- self.introspecting = False
- eventfilter = addon.create('GtkWidgetEventFilter',
- next_state=None,
- object_id=self._selected_widget,
- event_name=event_name)
# undo actions so they don't persist through step editing
- for action in self._tutorial.current_actions:
+ for action in self._state.get_action_list():
action.exit_editmode()
- self._tutorial.event(eventfilter)
- self._state_bubble.label = self._tutorial.state_name
self._hlmask.covered = None
self._propedit.action = None
self._activity.queue_draw()
@@ -159,63 +254,71 @@ class Creator(object):
self._eventmenu.popup(None, None, None, evt.button, evt.time)
self._activity.queue_draw()
- def set_intropecting(self, value):
- """
- Set whether creator is in UI introspection mode. Setting this will
- connect necessary handlers.
- @param value True to setup introspection handlers.
- """
- if bool(value) ^ bool(self._intro_mask):
- if value:
- self._intro_mask = overlayer.Mask(catch_events=True)
- self._intro_handle = self._intro_mask.connect_after(
- "button-press-event", self._intro_cb)
- self._activity._overlayer.put(self._intro_mask, 0, 0)
- else:
- self._intro_mask.catch_events = False
- self._intro_mask.disconnect(self._intro_handle)
- self._intro_handle = None
- self._activity._overlayer.remove(self._intro_mask)
- self._intro_mask = None
-
- def get_introspecting(self):
- """
- Whether creator is in UI introspection (catch all event) mode.
- @return True if introspection handlers are connected, or False if not.
- """
- return bool(self._intro_mask)
-
- introspecting = property(fset=set_intropecting, fget=get_introspecting)
-
- def _add_action_cb(self, widget, actiontype):
+ def _add_action_cb(self, widget, path):
"""Callback for the action creation toolbar tool"""
- action = addon.create(actiontype)
- if isinstance(action, actions.Action):
- action.enter_editmode()
- self._tutorial.action(action)
- # FIXME: replace following with event catching
- action._drag._eventbox.connect_after(
- "button-release-event", self._action_refresh_cb, action)
+ action_type = self._propedit._actions_icons[path][2]
+ action = addon.create(action_type)
+ action.enter_editmode()
+ self._state.add_action(action)
+ # FIXME: replace following with event catching
+ action._drag._eventbox.connect_after(
+ "button-release-event", self._action_refresh_cb, action)
+ self._overview.win.queue_draw()
+
+ def _add_event_cb(self, widget, path):
+ """Callback for the event creation toolbar tool"""
+ event_type = self._propedit._events_icons[path][2]
+ event = addon.create(event_type)
+ addonname = type(event).__name__
+ meta = addon.get_addon_meta(addonname)
+ for propname in meta['mandatory_props']:
+ prop = getattr(type(event), propname)
+ if isinstance(prop, properties.TUAMProperty):
+ selector = WidgetSelector(self._activity)
+ setattr(event, propname, selector.select())
+ elif isinstance(prop, properties.TGtkSignal):
+ try:
+ dlg = SignalInputDialog(self._activity,
+ text="Mandatory property",
+ field=propname,
+ addr=event.object_id)
+ setattr(event, propname, dlg.pop())
+ except AttributeError:
+ pass
+ elif isinstance(prop, properties.TStringProperty):
+ dlg = TextInputDialog(self._activity,
+ text="Mandatory property",
+ field=propname)
+ setattr(event, propname, dlg.pop())
+ else:
+ raise NotImplementedError()
+
+ event_filters = self._state.get_event_filter_list()
+ if event_filters:
+ # linearize tutorial by inserting state
+ new_state = State(name=str(self._state_counter))
+ self._state_counter += 1
+ self._state.clear_event_filters()
+ for evt_filt, next_state in event_filters:
+ new_state.add_event_filter(evt_filt, next_state)
+ self.set_next_state(self._state, event, new_state.name)
+ next_state = new_state.name
+ #event.set_next_state(new_state.name)
+ # blocks are shifted, full redraw is necessary
+ self._overview.win.queue_draw()
else:
- addonname = type(action).__name__
- meta = addon.get_addon_meta(addonname)
- had_introspect = False
- for propname in meta['mandatory_props']:
- prop = getattr(type(action), propname)
- if isinstance(prop, properties.TUAMProperty):
- had_introspect = True
- self.introspecting = True
- elif isinstance(prop, properties.TStringProperty):
- dlg = TextInputDialog(title="Mandatory property",
- field=propname)
- setattr(action, propname, dlg.pop())
- else:
- raise NotImplementedError()
-
- # FIXME: hack to reuse previous introspection code
- if not had_introspect:
- self._tutorial.event(action)
+ # append empty event only if edit not inserting between events
+ self.set_next_state(self._state, event, str(self._state_counter))
+ next_state = str(self._state_counter)
+ #event.set_next_state(str(self._state_counter))
+ new_state = State(name=str(self._state_counter))
+ self._state_counter += 1
+
+ self._state.add_event_filter(event, next_state)
+ self._tutorial.add_state(new_state)
+ self._overview.win.queue_draw()
+ self.set_insertion_point(new_state.name)
def _action_refresh_cb(self, widget, evt, action):
"""
@@ -230,44 +333,54 @@ class Creator(object):
"button-release-event", self._action_refresh_cb, action)
self._propedit.action = action
- def _add_step_cb(self, widget):
- """Callback for the "add step" tool"""
- self.introspecting = True
+ self._overview.win.queue_draw()
def _cleanup_cb(self, *args):
"""
Quit editing and cleanup interface artifacts.
"""
- self.introspecting = False
- eventfilter = filters.EventFilter(None)
# undo actions so they don't persist through step editing
- for action in self._tutorial.current_actions:
+ for action in self._state.get_action_list():
action.exit_editmode()
- self._tutorial.event(eventfilter)
- dlg = TextInputDialog(text=T("Enter a tutorial title."),
- field=T("Title"))
- tutorialName = ""
- while not tutorialName: tutorialName = dlg.pop()
- dlg.destroy()
-
- # prepare tutorial for serialization
- tuto = Tutorial(tutorialName, self._tutorial.fsm)
- bundle = bundler.TutorialBundler()
- bundle.write_metadata_file(tuto)
- bundle.write_fsm(self._tutorial.fsm)
+ dialog = gtk.MessageDialog(
+ parent=self._activity,
+ flags=gtk.DIALOG_MODAL,
+ type=gtk.MESSAGE_QUESTION,
+ buttons=gtk.BUTTONS_YES_NO,
+ message_format=T('Do you want to save before stopping edition?'))
+ do_save = dialog.run()
+ dialog.destroy()
+ if do_save == gtk.RESPONSE_YES:
+ self.save()
# remove UI remains
self._hlmask.covered = None
self._activity._overlayer.remove(self._hlmask)
- self._activity._overlayer.remove(self._state_bubble)
self._hlmask.destroy()
self._hlmask = None
- self._tooldialog.destroy()
self._propedit.destroy()
+ self._overview.destroy()
self._activity.queue_draw()
del self._activity._creator
+ def save(self, widget=None):
+ if not self.tuto:
+ dlg = TextInputDialog(self._activity,
+ text=T("Enter a tutorial title."),
+ field=T("Title"))
+ tutorialName = ""
+ while not tutorialName: tutorialName = dlg.pop()
+ dlg.destroy()
+
+ # prepare tutorial for serialization
+ self.tuto = Tutorial(tutorialName, self._tutorial)
+ bundle = bundler.TutorialBundler(self._guid)
+ self._guid = bundle.Guid
+ bundle.write_metadata_file(self.tuto)
+ bundle.write_fsm(self._tutorial)
+
+
def launch(*args, **kwargs):
"""
Launch and attach a creator to the currently running activity.
@@ -277,46 +390,53 @@ class Creator(object):
activity._creator = Creator(activity)
launch = staticmethod(launch)
-class EditToolBox(gtk.Window):
- """Helper toolbox class for managing action properties"""
- def __init__(self, parent, action=None):
- """
- Create the property edition toolbox and display it.
+class ToolBox(object):
+ def __init__(self, parent):
+ super(ToolBox, self).__init__()
+ self.__parent = parent
+ glade_file = os.path.join(__path__[0], 'ui', 'creator.glade')
+ self.tree = gtk.glade.XML(glade_file)
+ self.window = self.tree.get_widget('mainwindow')
+ self._propbox = self.tree.get_widget('propbox')
- @param parent the parent window of this toolbox, usually an activity
- @param action the action to introspect/edit
- """
- gtk.Window.__init__(self)
- self._action = None
- self.__parent = parent # private avoid gtk clash
-
- self.set_title("Action Properties")
- self.set_transient_for(parent)
- self.set_decorated(True)
- self.set_resizable(False)
- self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY)
- self.set_destroy_with_parent(True)
- self.set_deletable(False)
- self.set_size_request(200, 400)
-
- self._vbox = gtk.VBox()
- self.add(self._vbox)
- propwin = gtk.ScrolledWindow()
- propwin.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
- propwin.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
- self._vbox.pack_start(propwin)
- self._propbox = gtk.VBox(spacing=10)
- propwin.add(self._propbox)
-
- self.action = action
-
- sw = gtk.gdk.screen_width()
- sh = gtk.gdk.screen_height()
+ self.window.set_transient_for(parent)
- self.show_all()
- self.move(sw-10-200, (sh-400)/2)
-
- def refresh(self):
+ self._action = None
+ self._actions_icons = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str)
+ self._actions_icons.set_sort_column_id(0, gtk.SORT_ASCENDING)
+ self._events_icons = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str)
+ self._events_icons.set_sort_column_id(0, gtk.SORT_ASCENDING)
+
+ for toolname in addon.list_addons():
+ meta = addon.get_addon_meta(toolname)
+ iconfile = gtk.Image()
+ iconfile.set_from_file(icon.get_icon_file_name(meta['icon']))
+ img = iconfile.get_pixbuf()
+ label = format_multiline(meta['display_name'])
+
+ if meta['type'] == addon.TYPE_ACTION:
+ self._actions_icons.append((label, img, toolname, meta['display_name']))
+ else:
+ self._events_icons.append((label, img, toolname, meta['display_name']))
+
+ iconview1 = self.tree.get_widget('iconview1')
+ iconview1.set_model(self._actions_icons)
+ iconview1.set_text_column(0)
+ iconview1.set_pixbuf_column(1)
+ iconview1.set_tooltip_column(3)
+ iconview2 = self.tree.get_widget('iconview2')
+ iconview2.set_model(self._events_icons)
+ iconview2.set_text_column(0)
+ iconview2.set_pixbuf_column(1)
+ iconview2.set_tooltip_column(3)
+
+ self.window.show()
+
+ def destroy(self):
+ """ clean and free the toolbox """
+ self.window.destroy()
+
+ def refresh_properties(self):
"""Refresh property values from the selected action."""
if self._action is None:
return
@@ -344,12 +464,10 @@ class EditToolBox(gtk.Window):
def set_action(self, action):
"""Setter for the action property."""
if self._action is action:
- self.refresh()
+ self.refresh_properties()
return
- parent = self._propbox.get_parent()
- parent.remove(self._propbox)
- self._propbox = gtk.VBox(spacing=10)
- parent.add(self._propbox)
+ for old_prop in self._propbox.get_children():
+ self._propbox.remove(old_prop)
self._action = action
if action is None:
@@ -384,8 +502,8 @@ class EditToolBox(gtk.Window):
propwdg.set_text(str(propval))
row.pack_end(propwdg)
self._propbox.pack_start(row, expand=False)
- self._vbox.show_all()
- self.refresh()
+ self._propbox.show_all()
+ self.refresh_properties()
def get_action(self):
"""Getter for the action property"""
@@ -395,7 +513,10 @@ class EditToolBox(gtk.Window):
def _list_prop_changed(self, widget, evt, action, propname, idx):
try:
- getattr(action, propname)[idx] = int(widget.get_text())
+ #Save props as tuples so that they can be hashed
+ attr = list(getattr(action, propname))
+ attr[idx] = int(widget.get_text())
+ setattr(action, propname, tuple(attr))
except ValueError:
widget.set_text(str(getattr(action, propname)[idx]))
self.__parent._creator._action_refresh_cb(None, None, action)
@@ -407,9 +528,143 @@ class EditToolBox(gtk.Window):
setattr(action, propname, widget.get_value_as_int())
self.__parent._creator._action_refresh_cb(None, None, action)
+
+class WidgetSelector(object):
+ """
+ Allow selecting a widget from within a window without interrupting the
+ flow of the current call.
+
+ The selector will run on the specified window until either a widget
+ is selected or abort() gets called.
+ """
+ def __init__(self, window):
+ super(WidgetSelector, self).__init__()
+ self.window = window
+ self._intro_mask = None
+ self._intro_handle = None
+ self._select_handle = None
+ self._prelight = None
+
+ def select(self):
+ """
+ Starts selecting a widget, by grabbing control of the mouse and
+ highlighting hovered widgets until one is clicked.
+ @returns: a widget address or None
+ """
+ if not self._intro_mask:
+ self._prelight = None
+ self._intro_mask = overlayer.Mask(catch_events=True)
+ self._select_handle = self._intro_mask.connect_after(
+ "button-press-event", self._end_introspect)
+ self._intro_handle = self._intro_mask.connect_after(
+ "motion-notify-event", self._intro_cb)
+ self.window._overlayer.put(self._intro_mask, 0, 0)
+ self.window._overlayer.queue_draw()
+
+ while bool(self._intro_mask) and not gtk.main_iteration():
+ pass
+
+ return gtkutils.raddr_lookup(self._prelight)
+
+ def _end_introspect(self, widget, evt):
+ if evt.type == gtk.gdk.BUTTON_PRESS and self._prelight:
+ self._intro_mask.catch_events = False
+ self._intro_mask.disconnect(self._intro_handle)
+ self._intro_handle = None
+ self._intro_mask.disconnect(self._select_handle)
+ self._select_handle = None
+ self.window._overlayer.remove(self._intro_mask)
+ self._intro_mask = None
+ # for some reason, gtk may not redraw after this unless told to.
+ self.window.queue_draw()
+
+ def _intro_cb(self, widget, evt):
+ """
+ Callback for capture of widget events, when in introspect mode.
+ """
+ # widget has focus, let's hilight it
+ win = gtk.gdk.display_get_default().get_window_at_pointer()
+ if not win:
+ return
+ click_wdg = win[0].get_user_data()
+ if not click_wdg.is_ancestor(self.window._overlayer):
+ # as popups are not (yet) supported, it would break
+ # badly if we were to play with a widget not in the
+ # hierarchy.
+ return
+ for hole in self._intro_mask.pass_thru:
+ self._intro_mask.mask(hole)
+ self._intro_mask.unmask(click_wdg)
+ self._prelight = click_wdg
+
+ self.window.queue_draw()
+
+ def abort(self):
+ """
+ Ends the selection. The control will return to the select() caller
+ with a return value of None, as selection was aborted.
+ """
+ self._intro_mask.catch_events = False
+ self._intro_mask.disconnect(self._intro_handle)
+ self._intro_handle = None
+ self._intro_mask.disconnect(self._select_handle)
+ self._select_handle = None
+ self.window._overlayer.remove(self._intro_mask)
+ self._intro_mask = None
+ self._prelight = None
+
+class SignalInputDialog(gtk.MessageDialog):
+ def __init__(self, parent, text, field, addr):
+ """
+ Create a gtk signal selection dialog.
+
+ @param parent: the parent window this dialog should stay over.
+ @param text: the title of the dialog.
+ @param field: the field description of the dialog.
+ @param addr: the widget address from which to fetch signal list.
+ """
+ gtk.MessageDialog.__init__(self, parent,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+ gtk.MESSAGE_QUESTION,
+ gtk.BUTTONS_OK,
+ None)
+ self.set_markup(text)
+ self.model = gtk.ListStore(str)
+ widget = gtkutils.find_widget(parent, addr)
+ for signal_name in gobject.signal_list_names(widget):
+ self.model.append(row=(signal_name,))
+ self.entry = gtk.ComboBox(self.model)
+ cell = gtk.CellRendererText()
+ self.entry.pack_start(cell)
+ self.entry.add_attribute(cell, 'text', 0)
+ hbox = gtk.HBox()
+ lbl = gtk.Label(field)
+ hbox.pack_start(lbl, False)
+ hbox.pack_end(self.entry)
+ self.vbox.pack_end(hbox, True, True)
+ self.show_all()
+
+ def pop(self):
+ """
+ Show the dialog. It will run in it's own loop and return control
+ to the caller when a signal has been selected.
+
+ @returns: a signal name or None if no signal was selected
+ """
+ self.run()
+ self.hide()
+ iter = self.entry.get_active_iter()
+ if iter:
+ text = self.model.get_value(iter, 0)
+ return text
+ return None
+
+ def _dialog_done_cb(self, entry, response):
+ self.response(response)
+
class TextInputDialog(gtk.MessageDialog):
- def __init__(self, text, field):
- gtk.MessageDialog.__init__(self, None,
+ def __init__(self, parent, text, field):
+ gtk.MessageDialog.__init__(self, parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_QUESTION,
gtk.BUTTONS_OK,
@@ -433,4 +688,35 @@ class TextInputDialog(gtk.MessageDialog):
def _dialog_done_cb(self, entry, response):
self.response(response)
+def format_multiline(text, length=10, lines=3, line_separator='\n'):
+ """
+ Reformat a text to fit in a small space.
+
+ @param length: maximum char per line
+ @param lines: maximum number of lines
+ """
+ words = text.split(' ')
+ line = list()
+ return_val = []
+ linelen = 0
+
+ for word in words:
+ t_len = linelen+len(word)
+ if t_len < length:
+ line.append(word)
+ linelen = t_len+1 # count space
+ else:
+ if len(return_val)+1 < lines:
+ return_val.append(' '.join(line))
+ line = list()
+ linelen = 0
+ line.append(word)
+ else:
+ return_val.append(' '.join(line+['...']))
+ return line_separator.join(return_val)
+
+ return_val.append(' '.join(line))
+ return line_separator.join(return_val)
+
+
# vim:set ts=4 sts=4 sw=4 et:
diff --git a/tutorius/dbustools.py b/tutorius/dbustools.py
new file mode 100644
index 0000000..1b685d7
--- /dev/null
+++ b/tutorius/dbustools.py
@@ -0,0 +1,41 @@
+import logging
+LOGGER = logging.getLogger("sugar.tutorius.dbustools")
+
+def save_args(callable, *xargs, **xkwargs):
+ def __call(*args, **kwargs):
+ kw = dict()
+ kw.update(kwargs)
+ kw.update(xkwargs)
+ return callable(*(xargs+args), **kw)
+ return __call
+
+def ignore(*args):
+ LOGGER.debug("Unhandled asynchronous dbus call response with arguments: %s", str(args))
+
+def logError(error):
+ LOGGER.error("Unhandled asynchronous dbus call error: %s", error)
+
+def remote_call(callable, args, return_cb=None, error_cb=None, block=False):
+ reply_cb = return_cb or ignore
+ errhandler_cb = error_cb or logError
+ if block:
+ try:
+ ret_val = callable(*args)
+ LOGGER.debug("remote_call return arguments: %s", str(ret_val))
+ except Exception, e:
+ #Use the specified error handler even for blocking calls
+ errhandler_cb(e)
+
+ #Return value signature might be :
+ if ret_val is None:
+ #Nothing
+ return reply_cb()
+ elif type(ret_val) in (list, tuple):
+ #Several parameters
+ return reply_cb(*ret_val)
+ else:
+ #One parameter
+ return reply_cb(ret_val)
+ else:
+ callable(*args, reply_handler=reply_cb, error_handler=errhandler_cb)
+
diff --git a/tutorius/engine.py b/tutorius/engine.py
new file mode 100644
index 0000000..dda9f3f
--- /dev/null
+++ b/tutorius/engine.py
@@ -0,0 +1,48 @@
+import logging
+import dbus.mainloop.glib
+from jarabe.model import shell
+
+from sugar.tutorius.bundler import TutorialStore
+from sugar.bundle.activitybundle import ActivityBundle
+
+class Engine:
+ """
+ Driver for the execution of tutorials
+ """
+
+ def __init__(self):
+ # FIXME Probe management should be in the probe manager
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ #FIXME shell.get_model() will only be useful in the shell process
+ self._shell = shell.get_model()
+ self._tutorial = None
+
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ if self._tutorial:
+ self._tutorial.detach()
+ self._tutorial = None
+
+ store = TutorialStore()
+
+ #Get the active activity from the shell
+ activity = self._shell.get_active_activity()
+ self._tutorial = store.load_tutorial(tutorialID, bundle_path=activity.get_bundle_path())
+
+ #TProbes automatically use the bundle id, available from the ActivityBundle
+ bundle = ActivityBundle(activity.get_bundle_path())
+ self._tutorial.attach(bundle.get_bundle_id())
+
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ self._tutorial.detach()
+ self._tutorial = None
+
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ raise NotImplementedError("Unable to store tutorial state")
+
diff --git a/tutorius/filters.py b/tutorius/filters.py
index aa8c997..44621d5 100644
--- a/tutorius/filters.py
+++ b/tutorius/filters.py
@@ -15,13 +15,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-import gobject
-import gtk
import logging
logger = logging.getLogger("filters")
-from sugar.tutorius.gtkutils import find_widget
-from sugar.tutorius.services import ObjectStore
from sugar.tutorius import properties
@@ -30,31 +26,13 @@ class EventFilter(properties.TPropContainer):
Base class for an event filter
"""
- next_state = properties.TStringProperty("None")
-
- def __init__(self, next_state=None):
+ def __init__(self):
"""
Constructor.
- @param next_state name of the next state
"""
super(EventFilter, self).__init__()
- if next_state:
- self.next_state = next_state
self._callback = None
- def get_next_state(self):
- """
- Getter for the next state
- """
- return self.next_state
-
- def set_next_state(self, new_next_name):
- """
- Setter for the next state. Should only be used during construction of
- the event_fitler, not while the tutorial is running.
- """
- self.next_state = new_next_name
-
def install_handlers(self, callback, **kwargs):
"""
install_handlers is called for eventfilters to setup all
@@ -94,111 +72,3 @@ class EventFilter(properties.TPropContainer):
if self._callback:
self._callback(self)
-class TimerEvent(EventFilter):
- """
- TimerEvent is a special EventFilter that uses gobject
- timeouts to trigger a state change after a specified amount
- of time. It must be used inside a gobject main loop to work.
- """
- def __init__(self,next_state,timeout_s):
- """Constructor.
-
- @param next_state default EventFilter param, passed on to EventFilter
- @param timeout_s timeout in seconds
- """
- super(TimerEvent,self).__init__(next_state)
- self._timeout = timeout_s
- self._handler_id = None
-
- def install_handlers(self, callback, **kwargs):
- """install_handlers creates the timer and starts it"""
- super(TimerEvent,self).install_handlers(callback, **kwargs)
- #Create the timer
- self._handler_id = gobject.timeout_add_seconds(self._timeout, self._timeout_cb)
-
- def remove_handlers(self):
- """remove handler removes the timer"""
- super(TimerEvent,self).remove_handlers()
- if self._handler_id:
- try:
- #XXX What happens if this was already triggered?
- #remove the timer
- gobject.source_remove(self._handler_id)
- except:
- pass
-
- def _timeout_cb(self):
- """
- _timeout_cb triggers the eventfilter callback.
-
- It is necessary because gobject timers only stop if the callback they
- trigger returns False
- """
- self.do_callback()
- return False #Stops timeout
-
-class GtkWidgetTypeFilter(EventFilter):
- """
- Event Filter that listens for keystrokes on a widget
- """
- def __init__(self, next_state, object_id, text=None, strokes=None):
- """Constructor
- @param next_state default EventFilter param, passed on to EventFilter
- @param object_id object tree-ish identifier
- @param text resulting text expected
- @param strokes list of strokes expected
-
- At least one of text or strokes must be supplied
- """
- super(GtkWidgetTypeFilter, self).__init__(next_state)
- self._object_id = object_id
- self._text = text
- self._captext = ""
- self._strokes = strokes
- self._capstrokes = []
- self._widget = None
- self._handler_id = None
-
- def install_handlers(self, callback, **kwargs):
- """install handlers
- @param callback default EventFilter callback arg
- """
- super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs)
- logger.debug("~~~GtkWidgetTypeFilter install")
- activity = ObjectStore().activity
- if activity is None:
- logger.error("No activity")
- raise RuntimeWarning("no activity in the objectstore")
-
- self._widget = find_widget(activity, self._object_id)
- if self._widget:
- self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb)
- logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) )
-
- def remove_handlers(self):
- """remove handlers"""
- super(GtkWidgetTypeFilter, self).remove_handlers()
- #if an event was connected, disconnect it
- if self._handler_id:
- self._widget.handler_disconnect(self._handler_id)
- self._handler_id=None
-
- def __keypress_cb(self, widget, event, *args):
- """keypress callback"""
- logger.debug("~~~keypressed!")
- key = event.keyval
- keystr = event.string
- logger.debug("~~~Got key: " + str(key) + ":"+ keystr)
- self._capstrokes += [key]
- #TODO Treat other stuff, such as arrows
- if key == gtk.keysyms.BackSpace:
- self._captext = self._captext[:-1]
- else:
- self._captext = self._captext + keystr
-
- logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext))
- if not self._strokes is None and self._strokes in self._capstrokes:
- self.do_callback()
- if not self._text is None and self._text in self._captext:
- self.do_callback()
-
diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py
index 91b11f4..78e94ce 100644
--- a/tutorius/linear_creator.py
+++ b/tutorius/linear_creator.py
@@ -58,9 +58,8 @@ class LinearCreator(object):
# Set the next state name - there is no way the caller should have
# to deal with that.
next_state_name = "State %d" % (self.nb_state+1)
- event_filter.set_next_state(next_state_name)
state = State(self.state_name, action_list=self.current_actions,
- event_filter_list=[event_filter])
+ event_filter_list=[(event_filter, next_state_name),])
self.state_name = next_state_name
self.nb_state += 1
diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py
index 931949d..b967739 100644
--- a/tutorius/overlayer.py
+++ b/tutorius/overlayer.py
@@ -58,13 +58,13 @@ class Overlayer(gtk.Layout):
@param overlayed widget to be overlayed. Will be resized to full size.
"""
def __init__(self, overlayed=None):
- gtk.Layout.__init__(self)
+ super(Overlayer, self).__init__()
self._overlayed = overlayed
if overlayed:
self.put(overlayed, 0, 0)
- self.__realizer = self.connect("expose-event", self.__init_realized)
+ self.__realizer = self.connect_after("realize", self.__init_realized)
self.connect("size-allocate", self.__size_allocate)
self.show()
@@ -83,13 +83,13 @@ class Overlayer(gtk.Layout):
if hasattr(child, "draw_with_context"):
# if the widget has the CanvasDrawable protocol, use it.
child.no_expose = True
- gtk.Layout.put(self, child, x, y)
+ super(Overlayer, self).put(child, x, y)
# be sure to redraw or the overlay may not show
self.queue_draw()
- def __init_realized(self, widget, event):
+ def __init_realized(self, widget):
"""
Initializer to set once widget is realized.
Since an expose event is signaled only to realized widgets, we set this
@@ -157,7 +157,7 @@ class TextBubble(gtk.Widget):
A CanvasDrawableWidget drawing a round textbox and a tail pointing
to a specified widget.
"""
- def __init__(self, text, speaker=None, tailpos=[0,0]):
+ def __init__(self, text, speaker=None, tailpos=(0,0)):
"""
Creates a new cairo rendered text bubble.
@@ -199,7 +199,7 @@ class TextBubble(gtk.Widget):
# TODO fetch speaker coordinates
# draw bubble tail if present
- if self.tailpos != [0,0]:
+ if self.tailpos != (0,0):
context.move_to(xradius-width/4, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
context.line_to(xradius+width/4, yradius)
@@ -228,7 +228,7 @@ class TextBubble(gtk.Widget):
context.fill()
# bubble painting. Redrawing the inside after the tail will combine
- if self.tailpos != [0,0]:
+ if self.tailpos != (0,0):
context.move_to(xradius-width/4, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
context.line_to(xradius+width/4, yradius)
diff --git a/tutorius/properties.py b/tutorius/properties.py
index abf76e5..e3693fc 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -95,6 +95,25 @@ class TPropContainer(object):
"""
return object.__getattribute__(self, "_props").keys()
+ # Providing the hash methods necessary to use TPropContainers
+ # in a dictionary, according to their properties
+ def __hash__(self):
+ #Return a hash of properties (key, value) sorted by key
+ #We need to transform the list of property key, value lists into
+ # a tuple of key, value tuples
+ return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0])))))
+
+ def __eq__(self, e2):
+ return self._props == e2._props
+
+ # Adding methods for pickling and unpickling an object with
+ # properties
+ def __getstate__(self):
+ return self._props.copy()
+
+ def __setstate__(self, dict):
+ self._props.update(dict)
+
class TutoriusProperty(object):
"""
The base class for all actions' properties. The interface is the following :
@@ -145,19 +164,6 @@ class TAddonListProperty(TutoriusProperty):
"""
pass
-
- def get_constraints(self):
- """
- Returns the list of constraints associated to this property.
- """
- if self._constraints is None:
- self._constraints = []
- for i in dir(self):
- typ = getattr(self, i)
- if isinstance(typ, Constraint):
- self._constraints.append(i)
- return self._constraints
-
class TIntProperty(TutoriusProperty):
"""
Represents an integer. Can have an upper value limit and/or a lower value
@@ -207,8 +213,20 @@ class TArrayProperty(TutoriusProperty):
self.type = "array"
self.max_size_limit = MaxSizeConstraint(max_size_limit)
self.min_size_limit = MinSizeConstraint(min_size_limit)
- self.default = self.validate(value)
+ self.default = tuple(self.validate(value))
+ #Make this thing hashable
+ def __setstate__(self, state):
+ self.max_size_limit = MaxSizeConstraint(state["max_size_limit"])
+ self.min_size_limit = MinSizeConstraint(state["min_size_limit"])
+ self.value = state["value"]
+
+ def __getstate__(self):
+ return dict(
+ max_size_limit=self.max_size_limit.limit,
+ min_size_limit=self.min_size_limit.limit,
+ value=self.value,
+ )
class TColorProperty(TutoriusProperty):
"""
Represents a RGB color with 3 8-bit integer values.
@@ -287,8 +305,10 @@ class TUAMProperty(TutoriusProperty):
"""
Represents a widget of the interface by storing its UAM.
"""
- # TODO : Pending UAM check-in (LP 355199)
- pass
+ def __init__(self, value=None):
+ TutoriusProperty.__init__(self)
+
+ self.type = "uam"
class TAddonProperty(TutoriusProperty):
"""
@@ -311,14 +331,31 @@ class TAddonProperty(TutoriusProperty):
return super(TAddonProperty, self).validate(value)
raise ValueError("Expected TPropContainer instance as TaddonProperty value")
+class TGtkSignal(TutoriusProperty):
+ """
+ Represents a gobject signal for a GTK widget.
+ """
+ def __init__(self, value):
+ TutoriusProperty.__init__(self)
+ self.type = "gtk-signal"
+
+ self.default = self.validate(value)
+
class TAddonListProperty(TutoriusProperty):
"""
Reprensents an embedded tutorius Addon List Component.
See TAddonProperty
"""
def __init__(self):
- super(TAddonProperty, self).__init__()
+ TutoriusProperty.__init__(self)
self.type = "addonlist"
self.default = []
+ def validate(self, value):
+ if isinstance(value, list):
+ for component in value:
+ if not (isinstance(component, TPropContainer)):
+ raise ValueError("Expected a list of TPropContainer instances inside TAddonListProperty value, got a %s" % (str(type(component))))
+ return value
+ raise ValueError("Value proposed to TAddonListProperty is not a list")
diff --git a/tutorius/service.py b/tutorius/service.py
new file mode 100644
index 0000000..21f0cf1
--- /dev/null
+++ b/tutorius/service.py
@@ -0,0 +1,85 @@
+from engine import Engine
+import dbus
+
+from dbustools import remote_call
+
+_DBUS_SERVICE = "org.tutorius.Service"
+_DBUS_PATH = "/org/tutorius/Service"
+_DBUS_SERVICE_IFACE = "org.tutorius.Service"
+
+class Service(dbus.service.Object):
+ """
+ Global tutorius entry point to control the whole system
+ """
+
+ def __init__(self):
+ bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, _DBUS_PATH)
+
+ self._engine = None
+
+ def start(self):
+ """ Start the service itself
+ """
+ # For the moment there is nothing to do
+ pass
+
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="s", out_signature="")
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ if self._engine == None:
+ self._engine = Engine()
+ self._engine.launch(tutorialID)
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="", out_signature="")
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ self._engine.stop()
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="", out_signature="")
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ self._engine.pause()
+
+class ServiceProxy:
+ """ Proxy to connect to the Service object, abstracting the DBus interface"""
+
+ def __init__(self):
+ bus = dbus.SessionBus()
+ self._object = bus.get_object(_DBUS_SERVICE,_DBUS_PATH)
+ self._service = dbus.Interface(self._object, _DBUS_SERVICE_IFACE)
+
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ remote_call(self._service.launch, (tutorialID, ), block=False)
+
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ remote_call(self._service.stop, (), block=False)
+
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ remote_call(self._service.pause, (), block=False)
+
+if __name__ == "__main__":
+ import dbus.mainloop.glib
+ import gobject
+
+ loop = gobject.MainLoop()
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ s = Service()
+ loop.run()
+
diff --git a/tutorius/services.py b/tutorius/services.py
index 9ed2e50..e7b17d8 100644
--- a/tutorius/services.py
+++ b/tutorius/services.py
@@ -22,6 +22,9 @@ This module supplies services to be used by States, FSMs, Actions and Filters.
Services provided are:
-Access to the running activity
-Access to the running tutorial
+
+TODO: Passing the activity reference should be done by the Probe instead
+of being a global variable.
"""
diff --git a/tutorius/store.py b/tutorius/store.py
new file mode 100644
index 0000000..480c81b
--- /dev/null
+++ b/tutorius/store.py
@@ -0,0 +1,173 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import urllib
+
+class StoreProxy(object):
+ """
+ Implements a communication channel with the Tutorius Store, where tutorials
+ are shared from around the world. This proxy is meant to offer a one-stop
+ shop to implement all the requests that could be made to the Store.
+ """
+
+ def get_categories(self):
+ """
+ Returns all the categories registered in the store. Categories are used to
+ classify tutorials according to a theme. (e.g. Mathematics, History, etc...)
+
+ @return The list of category names stored on the server.
+ """
+ raise NotImplementedError("get_categories() not implemented")
+
+ def get_tutorials(self, keywords=None, category=None, startIndex=0, numResults=10, sortBy='name'):
+ """
+ Returns the list of tutorials that correspond to the given search criteria.
+
+ @param keywords The list of keywords that should be matched inside the tutorial title
+ or description. If None, the search will not filter the results
+ according to the keywords.
+ @param category The category in which to restrict the search.
+ @param startIndex The index in the result set from which to return results. This is
+ used to allow applications to fetch results one set at a time.
+ @param numResults The max number of results that can be returned
+ @param sortBy The field on which to sort the results
+ @return A list of tutorial meta-data that corresponds to the query
+ """
+ raise NotImplementedError("get_tutorials() not implemented")
+
+ def get_tutorial_collection(self, collection_name):
+ """
+ Returns a list of tutorials corresponding to the given collection name.
+ Collections can be groups like '5 most downloaded' or 'Top 10 ratings'.
+
+ @param collection_name The name of the collection from which we want the
+ meta-data
+ @return A list of tutorial meta-data corresponding to the given group
+ """
+ raise NotImplementedError("get_tutorial_collection() not implemented... yet!")
+
+ def get_latest_version(self, tutorial_id_list):
+ """
+ Returns the latest version number on the server, for each tutorial ID
+ in the list.
+
+ @param tutorial_id_list The list of tutorial IDs from which we want to
+ known the latest version number.
+ @return A dictionary having the tutorial ID as the key and the version
+ as the value.
+ """
+ raise NotImplementedError("get_latest_version() not implemented")
+
+ def download_tutorial(self, tutorial_id, version=None):
+ """
+ Fetches the tutorial file from the server and returns the
+
+ @param tutorial_id The tutorial that we want to get
+ @param version The version number that we want to download. If None,
+ the latest version will be downloaded.
+ @return The downloaded file itself (an in-memory representation of the file,
+ not a path to it on the disk)
+
+ TODO : We should decide if we're saving to disk or in mem.
+ """
+ raise NotImplementedError("downloadTutorial() not implemented")
+
+ def login(self, username, password):
+ """
+ Logs in the user on the store and saves the login status in the proxy
+ state. After a successful logon, the operation requiring a login will
+ be successful.
+
+ @return True if the login was successful, False otherwise
+ """
+ raise NotImplementedError("login() not implemented yet")
+
+ def close_session(self):
+ """
+ Ends the user's session on the server and changes the state of the proxy
+ to disallow the calls to the store that requires to be logged in.
+
+ @return True if the user was disconnected, False otherwise
+ """
+ raise NotImplementedError("close_session() not implemented yet")
+
+ def get_session_id(self):
+ """
+ Gives the current session ID cached in the Store Proxy, or returns
+ None is the user is not logged yet.
+
+ @return The current session's ID, or None if the user is not logged
+ """
+ raise NotImplementedError("get_session_id() not implemented yet")
+
+ def rate(self, value, tutorial_store_id):
+ """
+ Sends a rating for the given tutorial.
+
+ This function requires the user to be logged in.
+
+ @param value The value of the rating. It must be an integer with a value
+ from 1 to 5.
+ @param tutorial_store_id The ID of the tutorial that was rated
+ @return True if the rating was sent to the Store, False otherwise.
+ """
+ raise NotImplementedError("rate() not implemented")
+
+ def publish(self, tutorial):
+ """
+ Sends a tutorial to the store.
+
+ This function requires the user to be logged in.
+
+ @param tutorial The tutorial file to be sent. Note that this is the
+ content itself and not the path to the file.
+ @return True if the tutorial was sent correctly, False otherwise.
+ """
+ raise NotImplemetedError("publish() not implemented")
+
+ def unpublish(self, tutorial_store_id):
+ """
+ Removes a tutorial from the server. The user in the current session
+ needs to be the creator for it to be unpublished. This will remove
+ the file from the server and from all its collections and categories.
+
+ This function requires the user to be logged in.
+
+ @param tutorial_store_id The ID of the tutorial to be removed
+ @return True if the tutorial was properly removed from the server
+ """
+ raise NotImplementedError("unpublish() not implemeted")
+
+ def update_published_tutorial(self, tutorial_id, tutorial):
+ """
+ Sends the new content for the tutorial with the given ID.
+
+ This function requires the user to be logged in.
+
+ @param tutorial_id The ID of the tutorial to be updated
+ @param tutorial The bundled tutorial file content (not a path!)
+ @return True if the tutorial was sent and updated, False otherwise
+ """
+ raise NotImplementedError("update_published_tutorial() not implemented yet")
+
+ def register_new_user(self, user_info):
+ """
+ Creates a new user from the given user information.
+
+ @param user_info A structure containing all the data required to do a login.
+ @return True if the new account was created, false otherwise
+ """
+ raise NotImplementedError("register_new_user() not implemented")
diff --git a/tutorius/uam/__init__.py b/tutorius/uam/__init__.py
index 7cf5671..bcd67e1 100644
--- a/tutorius/uam/__init__.py
+++ b/tutorius/uam/__init__.py
@@ -65,7 +65,8 @@ for subscheme in [".".join([SCHEME,s]) for s in __parsers]:
class SchemeError(Exception):
def __init__(self, message):
Exception.__init__(self, message)
- self.message = message
+ ## Commenting this line as it is causing an error in the tests
+ ##self.message = message
def parse_uri(uri):
diff --git a/tutorius/ui/creator.glade b/tutorius/ui/creator.glade
new file mode 100644
index 0000000..1c9669d
--- /dev/null
+++ b/tutorius/ui/creator.glade
@@ -0,0 +1,209 @@
+<?xml version="1.0"?>
+<glade-interface>
+ <!-- interface-requires gtk+ 2.16 -->
+ <!-- interface-naming-policy project-wide -->
+ <widget class="GtkWindow" id="mainwindow">
+ <property name="width_request">300</property>
+ <property name="height_request">500</property>
+ <property name="title" translatable="yes">Toolbox</property>
+ <property name="resizable">False</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="default_width">200</property>
+ <property name="default_height">500</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">True</property>
+ <property name="focus_on_map">False</property>
+ <property name="deletable">False</property>
+ <signal name="destroy" handler="on_mainwindow_destroy"/>
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">5</property>
+ <child>
+ <widget class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="spacing">5</property>
+ <property name="layout_style">start</property>
+ <child>
+ <widget class="GtkButton" id="button2">
+ <property name="label">gtk-save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_save_clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="button4">
+ <property name="label">gtk-quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_quit_clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <widget class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="resize_mode">queue</property>
+ <child>
+ <widget class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkExpander" id="expander1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <widget class="GtkIconView" id="iconview1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="columns">2</property>
+ <property name="row_spacing">0</property>
+ <property name="column_spacing">0</property>
+ <property name="item_padding">0</property>
+ <signal name="item_activated" handler="on_action_activate"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">actions</property>
+ </widget>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkExpander" id="expander2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <widget class="GtkIconView" id="iconview2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="columns">2</property>
+ <property name="row_spacing">0</property>
+ <property name="column_spacing">0</property>
+ <property name="item_padding">0</property>
+ <signal name="item_activated" handler="on_event_activate"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">events</property>
+ </widget>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="propbox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">10</property>
+ <child>
+ <placeholder/>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHButtonBox" id="hbuttonbox2">
+ <property name="visible">True</property>
+ <property name="spacing">5</property>
+ <property name="layout_style">start</property>
+ <child>
+ <widget class="GtkButton" id="button1">
+ <property name="label">gtk-media-record</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="button3">
+ <property name="label">gtk-media-stop</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+</glade-interface>
diff --git a/tutorius/vault.py b/tutorius/vault.py
new file mode 100644
index 0000000..9215e8d
--- /dev/null
+++ b/tutorius/vault.py
@@ -0,0 +1,861 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Jean-Christophe Savard <savard.jean.christophe@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+"""
+This module contains all the data handling class of Tutorius
+"""
+
+import logging
+import os
+import shutil
+import tempfile
+import uuid
+import xml.dom.minidom
+from xml.dom import NotFoundErr
+import zipfile
+
+from sugar.tutorius import addon
+from sugar.tutorius.core import Tutorial, State, FiniteStateMachine
+from ConfigParser import SafeConfigParser
+
+logger = logging.getLogger("tutorius")
+
+# this is where user installed/generated tutorials will go
+def _get_store_root():
+ profile_name = os.getenv("SUGAR_PROFILE") or "default"
+ return os.path.join(os.getenv("HOME"),
+ ".sugar",profile_name,"tutorius","data")
+# this is where activity bundled tutorials should be, under the activity bundle
+def _get_bundle_root():
+ """
+ Return the path of the bundled activity, or None if not applicable.
+ """
+ if os.getenv("SUGAR_BUNDLE_PATH") != None:
+ return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data")
+ else:
+ return None
+
+INI_ACTIVITY_SECTION = "RELATED_ACTIVITIES"
+INI_METADATA_SECTION = "GENERAL_METADATA"
+INI_GUID_PROPERTY = "guid"
+INI_NAME_PROPERTY = "name"
+INI_XML_FSM_PROPERTY = "fsm_filename"
+INI_VERSION_PROPERTY = 'version'
+INI_FILENAME = "meta.ini"
+TUTORIAL_FILENAME = "tutorial.xml"
+NODE_COMPONENT = "Component"
+NODE_SUBCOMPONENT = "property"
+NODE_SUBCOMPONENTLIST = "listproperty"
+NEXT_STATE_ATTR = "next_state"
+
+class Vault(object):
+
+ ## Vault internal functions :
+ @staticmethod
+ def list_available_tutorials(activity_name = None, activity_vers = 0):
+ """
+ Generate the list of all tutorials present on disk for a
+ given activity.
+
+ @param activity_name the name of the activity associated with this tutorial. None means ALL activities
+ @param activity_vers the version number of the activity to find tutorail for. 0 means find for ANY version. Ifactivity_ame is None, version number is not used
+ @returns a map of tutorial {names : GUID}.
+ """
+ # check both under the activity data and user installed folders
+ if _get_bundle_root() != None:
+ paths = [_get_store_root(), _get_bundle_root()]
+ else:
+ paths = [_get_store_root()]
+
+ tutoGuidName = {}
+
+ for repository in paths:
+ # (our) convention dictates that tutorial folders are named
+ # with their GUID (for unicity)
+ try:
+ for tuto in os.listdir(repository):
+ parser = SafeConfigParser()
+ file = parser.read(os.path.join(repository, tuto, INI_FILENAME))
+ if file != []:
+ # If parser can read at least section
+ guid = parser.get(INI_METADATA_SECTION, INI_GUID_PROPERTY)
+ name = parser.get(INI_METADATA_SECTION, INI_NAME_PROPERTY)
+ activities = parser.options(INI_ACTIVITY_SECTION)
+ # enforce matching activity name AND version, as UI changes
+ # break tutorials. We may lower this requirement when the
+ # UAM gets less dependent on the widget order.
+ # Also note property names are always stored lowercase.
+ if (activity_name != None) and (activity_name.lower() in activities):
+ version = parser.get(INI_ACTIVITY_SECTION, activity_name)
+ if (activity_vers == version) or (activity_vers == 0):
+ tutoGuidName[guid] = name
+ elif (activity_name == None):
+ tutoGuidName[guid] = name
+ except OSError:
+ # the repository may not exist. Continue scanning
+ pass
+
+ return tutoGuidName
+
+ ## Vault interface functions :
+ @staticmethod
+ def installTutorials(path, zip_file_name, forceinstall=False):
+ """
+ Extract the tutorial files in the ZIPPED tutorial archive at the
+ specified path and add them inside the vault. This should remove any previous
+ version of this tutorial, if there's any. On the opposite, if we are
+ trying to install an earlier version, the function will return 1 if
+ forceInstall is not set to true.
+
+ @params path The path where the zipped tutorial archive is present
+ @params forceinstall A flag that indicate if we need to force overwrite
+ of a tutorial even if is version number is lower than the existing one.
+
+ @returns 0 if it worked, 1 if the user needs to confirm the installation
+ and 2 to mean an error happened
+ """
+ # TODO : Check with architecture team for exception vs error returns
+
+ # test if the file is a valid pkzip file
+ if zipfile.is_zipfile(os.path.join(path, zip_file_name)) != True:
+ assert False, "Error : The given file is not a valid PKZip file"
+
+ # unpack the zip archive
+ zfile = zipfile.ZipFile(os.path.join(path, zip_file_name), "r" )
+
+ temp_path = tempfile.mkdtemp(dir=_get_store_root())
+ zfile.extractall(temp_path)
+
+ # get the tutorial file
+ ini_file_path = os.path.join(temp_path, INI_FILENAME)
+ ini_file = SafeConfigParser()
+ ini_file.read(ini_file_path)
+
+ # get the tutorial guid
+ guid = ini_file.get(INI_METADATA_SECTION, INI_GUID_PROPERTY)
+
+ # Check if tutorial already exist
+ tutorial_path = os.path.join(_get_store_root(), guid)
+ if os.path.isdir(tutorial_path) == False:
+ # Copy the tutorial in the Vault
+ shutil.copytree(temp_path, tutorial_path)
+
+ else:
+ # Check the version of the existing tutorial
+ existing_version = ini_file.get(INI_METADATA_SECTION, INI_VERSION_PROPERTY)
+ # Check the version of the new tutorial
+ new_ini_file = SafeConfigParser()
+ new_ini_file.read(os.path.join(tutorial_path, INI_FILENAME))
+ new_version = new_ini_file.get(INI_METADATA_SECTION, INI_VERSION_PROPERTY)
+
+ if new_version < existing_version and forceinstall == False:
+ # Version of new tutorial is older and forceinstall is false, return exception
+ return 1
+ else :
+ # New tutorial is newer or forceinstall flag is set, can overwrite the existing tutorial
+ shutil.rmtree(tutorial_path)
+ shutil.copytree(temp_path, tutorial_path)
+
+ # Remove temp data
+ shutil.rmtree(temp_path)
+
+ return 0
+
+ @staticmethod
+ def query(keyword=[], relatedActivityNames=[], category=[]):
+ """
+ Returns the list of tutorials that corresponds to the specified parameters.
+
+ @returns a list of Tutorial meta-data (TutorialID, Description,
+ Rating, Category, PublishState, etc...)
+ TODO : Search for tuto caracterised by the entry : OR between [], and between each
+
+ The returned dictionnary is of this format : key = property name, value = property value
+ The dictionnary also contain one dictionnary element whose key is the string 'activities'
+ and whose value is another dictionnary of this form : key = related activity name,
+ value = related activity version number
+ """
+
+ # Temp solution for returning all tutorials metadata
+
+ tutorial_list = []
+ tuto_guid_list = []
+ ini_file = SafeConfigParser()
+ if keyword == [] and relatedActivityNames == [] and category == []:
+ # get all tutorials tuples (name:guid) for all activities and version
+ tuto_dict = Vault.list_available_tutorials()
+ for id in tuto_dict.keys():
+ tuto_guid_list.append(id)
+
+ # Find .ini metadata files with the guid list
+
+ # Get the guid from the tuto tuples
+ for guid in tuto_guid_list:
+ # Create a dictionnary containing the metadata and also
+ # another dictionnary containing the tutorial Related Acttivities,
+ # and add it to a list
+
+ # Create a TutorialBundler object from the guid
+ bundler = TutorialBundler(guid)
+ # Find the .ini file path for this guid
+ ini_file_path = bundler.get_tutorial_path(guid)
+ # Read the .ini file
+ ini_file.read(os.path.join(ini_file_path, 'meta.ini'))
+
+ metadata_dictionnary = {}
+ related_act_dictionnary = {}
+ metadata_list = ini_file.options(INI_METADATA_SECTION)
+ for metadata_name in metadata_list:
+ # Create a dictionnary of tutorial metadata
+ metadata_dictionnary[metadata_name] = ini_file.get(INI_METADATA_SECTION, metadata_name)
+ # Get Related Activities data from.ini files
+ related_act_list = ini_file.options(INI_ACTIVITY_SECTION)
+ for related_act in related_act_list:
+ # For related activites, the format is : key = activity name, value = activity version
+ related_act_dictionnary[related_act] = ini_file.get(INI_ACTIVITY_SECTION, related_act)
+
+ # Add Related Activities dictionnary to metadata dictionnary
+ metadata_dictionnary['activities'] = related_act_dictionnary
+
+ # Add this dictionnary to tutorial list
+ tutorial_list.append(metadata_dictionnary)
+
+ # Return tutorial list
+ return tutorial_list
+
+ @staticmethod
+ def loadTutorial(Guid):
+ """
+ Creates an executable version of a tutorial from its saved representation.
+ @returns an executable representation of a tutorial
+ """
+
+ bundle = TutorialBundler(Guid)
+ bundle_path = bundle.get_tutorial_path(Guid)
+ config = SafeConfigParser()
+ config.read(os.path.join(bundle_path, INI_FILENAME))
+
+ serializer = XMLSerializer()
+
+ name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY)
+ fsm = serializer.load_fsm(Guid, bundle_path)
+
+ tuto = Tutorial(name, fsm)
+ return tuto
+
+ @staticmethod
+ def saveTutorial(tutorial, metadata_dict):
+ """
+ Creates a persistent version of a tutorial in the Vault.
+ @returns true if the tutorial was saved correctly
+ """
+
+ # Get the tutorial guid from metadata dictionnary
+ guid = metadata_dict[INI_GUID_PROPERTY]
+
+ # Check if tutorial already exist
+ tutorial_path = os.path.join(_get_store_root(), guid)
+ if os.path.isdir(tutorial_path) == False:
+
+ # Serialize the tutorial and write it to disk
+ xml_ser = XMLSerializer()
+ os.makedirs(tutorial_path)
+ xml_ser.save_fsm(tutorial.state_machine, TUTORIAL_FILENAME, tutorial_path)
+
+ # Create the metadata file
+ ini_file_path = os.path.join(tutorial_path, "meta.ini")
+ parser = SafeConfigParser()
+ parser.add_section(INI_METADATA_SECTION)
+ for key, value in metadata_dict.items():
+ if key != 'activities':
+ parser.set(INI_METADATA_SECTION, key, value)
+ else:
+ related_activities_dict = value
+ parser.add_section(INI_ACTIVITY_SECTION)
+ for related_key, related_value in related_activities_dict.items():
+ parser.set(INI_ACTIVITY_SECTION, related_key, related_value)
+
+ # Write the file to disk
+ with open(ini_file_path, 'wb') as configfile:
+ parser.write(configfile)
+
+ else:
+ # Error, tutorial already exist
+ return False
+
+ # TODO : wait for Ben input on how to unpublish tuto before coding this function
+ # For now, no unpublishing will occur.
+
+
+ @staticmethod
+ def deleteTutorial(Tutorial):
+ """
+ Removes the tutorial from the Vault. It will unpublish the tutorial if need be,
+ and it will also wipe it from the persistent storage.
+ @returns true is the tutorial was deleted from the Vault
+ """
+ bundle = TutorialBundler(Guid)
+ bundle_path = bundle.get_tutorial_path(Guid)
+
+ # TODO : Need also to unpublish tutorial, need to interact with webservice module
+
+ shutil.rmtree(bundle_path)
+ if os.path.isdir(bundle_path) == False:
+ return True
+ else:
+ return False
+
+
+class Serializer(object):
+ """
+ Interface that provide serializing and deserializing of the FSM
+ used in the tutorials to/from disk. Must be inherited.
+ """
+
+ def save_fsm(self,fsm):
+ """
+ Save fsm to disk. If a GUID parameter is provided, the existing GUID is
+ located in the .ini files in the store root and bundle root and
+ the corresponding FSM is/are overwritten. If the GUId is not found, an
+ exception occur. If no GUID is provided, FSM is written in a new file
+ in the store root.
+ """
+ raise NotImplementedError()
+
+ def load_fsm(self):
+ """
+ Load fsm from disk.
+ """
+ raise NotImplementedError()
+
+class XMLSerializer(Serializer):
+ """
+ Class that provide serializing and deserializing of the FSM
+ used in the tutorials to/from a .xml file. Inherit from Serializer
+ """
+
+ def _create_state_dict_node(self, state_dict, doc):
+ """
+ Create and return a xml Node from a State dictionnary.
+ """
+ statesList = doc.createElement("States")
+ for state_name, state in state_dict.items():
+ stateNode = doc.createElement("State")
+ statesList.appendChild(stateNode)
+ stateNode.setAttribute("Name", state_name)
+ actionsList = stateNode.appendChild(self._create_action_list_node(state.get_action_list(), doc))
+ eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc))
+ return statesList
+
+ def _create_addon_component_node(self, parent_attr_name, comp, doc):
+ """
+ Takes a component that is embedded in another component (e.g. the content
+ of a OnceWrapper) and encapsulate it in a node with the property name.
+
+ e.g.
+ <Component Class="OnceWrapper">
+ <property name="addon">
+ <Component Class="BubbleMessage" message="'Hi!'" position="[12,32]"/>
+ </property>
+ </Component>
+
+ When reloading this node, we should look up the property name for the parent
+ in the attribute of the node, then examine the subnode to create the addon
+ object itself.
+
+ @param parent_attr_name The name of the parent's attribute for this addon
+ e.g. the OnceWrapper has the action attribute, which corresponds to a
+ sub-action it must execute once.
+ @param comp The component node itself
+ @param doc The XML document root (only used to create the nodes)
+ @returns A NODE_SUBCOMPONENT node, with the property attribute and a sub node
+ that represents another component.
+ """
+ subCompNode = doc.createElement(NODE_SUBCOMPONENT)
+ subCompNode.setAttribute("name", parent_attr_name)
+
+ subNode = self._create_component_node(comp, doc)
+
+ subCompNode.appendChild(subNode)
+
+ return subCompNode
+
+ def _create_addonlist_component_node(self, parent_attr_name, comp_list, doc):
+ """
+ Takes a list of components that are embedded in another component (ex. the
+ content of a ChainAction) and encapsulate them in a node with the property
+ name.
+
+ e.g.
+ <Component Class="ChainAction">
+ <listproperty name="actions">
+ <Component Class="BubbleMessage" message="'Hi!'" position="[15,35]"/>
+ <Component Class="DialogMessage" message="'Multi-action!'" position="[45,10]"/>
+ </listproperty>
+ </Component>
+
+ When reloading this node, we should look up the property name for the parent
+ in the the attribute of the node, then rebuild the list by appending the
+ content of all the subnodes.
+
+ @param parent_attr_name The name of the parent component's property
+ @param comp_list A list of components that comprise the property
+ @param doc The XML document root (only for creating new nodes)
+ @returns A NODE_SUBCOMPONENTLIST node with the property attribute
+ """
+ subCompListNode = doc.createElement(NODE_SUBCOMPONENTLIST)
+ subCompListNode.setAttribute("name", parent_attr_name)
+
+ for comp in comp_list:
+ compNode = self._create_component_node(comp, doc)
+ subCompListNode.appendChild(compNode)
+
+ return subCompListNode
+
+ def _create_component_node(self, comp, doc):
+ """
+ Takes a single component (action or eventfilter) and transforms it
+ into a xml node.
+
+ @param comp A single component
+ @param doc The XML document root (used to create nodes only
+ @return A XML Node object with the component tag name
+ """
+ compNode = doc.createElement(NODE_COMPONENT)
+
+ # Write down just the name of the Action class as the Class
+ # property --
+ compNode.setAttribute("Class",type(comp).__name__)
+
+ # serialize all tutorius properties
+ for propname in comp.get_properties():
+ propval = getattr(comp, propname)
+ if getattr(type(comp), propname).type == "addonlist":
+ compNode.appendChild(self._create_addonlist_component_node(propname, propval, doc))
+ elif getattr(type(comp), propname).type == "addon":
+ #import rpdb2; rpdb2.start_embedded_debugger('pass')
+ compNode.appendChild(self._create_addon_component_node(propname, propval, doc))
+ else:
+ # repr instead of str, as we want to be able to eval() it into a
+ # valid object.
+ compNode.setAttribute(propname, repr(propval))
+
+ return compNode
+
+ def _create_action_list_node(self, action_list, doc):
+ """
+ Create and return a xml Node from a Action list.
+
+ @param action_list A list of actions
+ @param doc The XML document root (used to create new nodes only)
+ @return A XML Node object with the Actions tag name and a serie of
+ Action children
+ """
+ actionsList = doc.createElement("Actions")
+ for action in action_list:
+ # Create the action node
+ actionNode = self._create_component_node(action, doc)
+ # Append it to the list
+ actionsList.appendChild(actionNode)
+
+ return actionsList
+
+ def _create_event_filters_node(self, event_filters, doc):
+ """
+ Create and return a xml Node from an event filters.
+ """
+ eventFiltersList = doc.createElement("EventFiltersList")
+ for event, state in event_filters:
+ eventFilterNode = self._create_component_node(event, doc)
+ eventFilterNode.setAttribute(NEXT_STATE_ATTR, str(state))
+ eventFiltersList.appendChild(eventFilterNode)
+
+ return eventFiltersList
+
+ def save_fsm(self, fsm, xml_filename, path):
+ """
+ Save fsm to disk, in the xml file specified by "xml_filename", in the
+ "path" folder. If the specified file doesn't exist, it will be created.
+ """
+ self.doc = doc = xml.dom.minidom.Document()
+ fsm_element = doc.createElement("FSM")
+ doc.appendChild(fsm_element)
+ fsm_element.setAttribute("Name", fsm.name)
+ fsm_element.setAttribute("StartStateName", fsm.start_state_name)
+ statesDict = fsm_element.appendChild(self._create_state_dict_node(fsm._states, doc))
+
+ fsm_actions_node = self._create_action_list_node(fsm.actions, doc)
+ fsm_actions_node.tagName = "FSMActions"
+ actionsList = fsm_element.appendChild(fsm_actions_node)
+
+ file_object = open(os.path.join(path, xml_filename), "w")
+ file_object.write(doc.toprettyxml())
+ file_object.close()
+
+ def _get_direct_descendants_by_tag_name(self, node, name):
+ """
+ Searches in the list of direct descendants of a node to find all the node
+ that have the given name.
+
+ This is used because the Document.getElementsByTagName() function returns the
+ list of all the descendants (whatever their distance to the start node) that
+ have that name. In the case of complex components, we absolutely need to inspect
+ a single layer of the tree at the time.
+
+ @param node The node from which we want the direct descendants with a particular
+ name
+ @param name The name of the node
+ @returns A list, possibly empty, of direct descendants of node that have this name
+ """
+ return_list = []
+ for childNode in node.childNodes:
+ if childNode.nodeName == name:
+ return_list.append(childNode)
+ return return_list
+
+
+## def _load_xml_properties(self, properties_elem):
+## """
+## Changes a list of properties into fully instanciated properties.
+##
+## @param properties_elem An XML element reprensenting a list of
+## properties
+## """
+## return []
+
+ def _load_xml_event_filters(self, filters_elem):
+ """
+ Loads up a list of Event Filters.
+
+ @param filters_elem An XML Element representing a list of event filters
+ """
+ transition_list = []
+ event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT)
+ new_event_filter = None
+
+ for event_filter in event_filter_element_list:
+ next_state = event_filter.getAttribute(NEXT_STATE_ATTR)
+ try:
+ event_filter.removeAttribute(NEXT_STATE_ATTR)
+ except NotFoundErr:
+ next_state = None
+ new_event_filter = self._load_xml_component(event_filter)
+
+ if new_event_filter is not None:
+ transition_list.append((new_event_filter, next_state))
+
+ return transition_list
+
+ def _load_xml_subcomponents(self, node, properties):
+ """
+ Loads all the subcomponent node below the given node and inserts them with
+ the right property name inside the properties dictionnary.
+
+ @param node The parent node that contains one or many property nodes.
+ @param properties A dictionnary where the subcomponent property names
+ and the instantiated components will be stored
+ @returns Nothing. The properties dict will contain the property->comp mapping.
+ """
+ subCompList = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT)
+
+ for subComp in subCompList:
+ property_name = subComp.getAttribute("name")
+ internal_comp_node = self._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0]
+ internal_comp = self._load_xml_component(internal_comp_node)
+ properties[str(property_name)] = internal_comp
+
+ def _load_xml_subcomponent_lists(self, node, properties):
+ """
+ Loads all the subcomponent lists below the given node and stores them
+ under the correct property name for that node.
+
+ @param node The node from which we want to read the subComponent lists
+ @param properties The dictionnary that will contain the mapping of prop->subCompList
+ @returns Nothing. The values are returns inside the properties dict.
+ """
+ listOf_subCompListNode = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST)
+ for subCompListNode in listOf_subCompListNode:
+ property_name = subCompListNode.getAttribute("name")
+ subCompList = []
+ for subCompNode in self._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT):
+ subComp = self._load_xml_component(subCompNode)
+ subCompList.append(subComp)
+ properties[str(property_name)] = subCompList
+
+ def _load_xml_component(self, node):
+ """
+ Loads a single addon component instance from an Xml node.
+
+ @param node The component XML Node to transform
+ object
+ @return The addon component object of the correct type according to the XML
+ description
+ """
+ class_name = node.getAttribute("Class")
+
+ properties = {}
+
+ for prop in node.attributes.keys():
+ if prop == "Class" : continue
+ # security : keep sandboxed
+ properties[str(prop)] = eval(node.getAttribute(prop))
+
+ # Read the complex attributes
+ self._load_xml_subcomponents(node, properties)
+ self._load_xml_subcomponent_lists(node, properties)
+
+ new_action = addon.create(class_name, **properties)
+
+ if not new_action:
+ return None
+
+ return new_action
+
+ def _load_xml_actions(self, actions_elem):
+ """
+ Transforms an Actions element into a list of instanciated Action.
+
+ @param actions_elem An XML Element representing a list of Actions
+ """
+ reformed_actions_list = []
+ actions_element_list = self._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT)
+
+ for action in actions_element_list:
+ new_action = self._load_xml_component(action)
+
+ reformed_actions_list.append(new_action)
+
+ return reformed_actions_list
+
+ def _load_xml_states(self, states_elem):
+ """
+ Takes in a States element and fleshes out a complete list of State
+ objects.
+
+ @param states_elem An XML Element that represents a list of States
+ """
+ reformed_state_list = []
+ # item(0) because there is always only one <States> tag in the xml file
+ # so states_elem should always contain only one element
+ states_element_list = states_elem.item(0).getElementsByTagName("State")
+
+ for state in states_element_list:
+ stateName = state.getAttribute("Name")
+ # Using item 0 in the list because there is always only one
+ # Actions and EventFilterList element per State node.
+ actions_list = self._load_xml_actions(state.getElementsByTagName("Actions")[0])
+ event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList")[0])
+ reformed_state_list.append(State(stateName, actions_list, event_filters_list))
+
+ return reformed_state_list
+
+ def load_xml_fsm(self, fsm_elem):
+ """
+ Takes in an XML element representing an FSM and returns the fully
+ crafted FSM.
+
+ @param fsm_elem The XML element that describes a FSM
+ """
+ # Load the FSM's name and start state's name
+ fsm_name = fsm_elem.getAttribute("Name")
+
+ fsm_start_state_name = None
+ try:
+ fsm_start_state_name = fsm_elem.getAttribute("StartStateName")
+ except:
+ pass
+
+ fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name)
+
+ # Load the states
+ states = self._load_xml_states(fsm_elem.getElementsByTagName("States"))
+ for state in states:
+ fsm.add_state(state)
+
+ # Load the actions on this FSM
+ actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions")[0])
+ for action in actions:
+ fsm.add_action(action)
+
+ # Load the event filters
+ events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0])
+ for event, next_state in events:
+ fsm.add_event_filter(event, next_state)
+
+ return fsm
+
+
+ def load_fsm(self, guid, path=None):
+ """
+ Load fsm from xml file whose .ini file guid match argument guid.
+ """
+ # Fetch the directory (if any)
+ bundler = TutorialBundler(guid)
+ tutorial_dir = bundler.get_tutorial_path(guid)
+
+ # Open the XML file
+ tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME)
+
+ xml_dom = xml.dom.minidom.parse(tutorial_file)
+
+ fsm_elem = xml_dom.getElementsByTagName("FSM")[0]
+
+ return self.load_xml_fsm(fsm_elem)
+
+
+class TutorialBundler(object):
+ """
+ This class provide the various data handling methods useable by the tutorial
+ editor.
+ """
+
+ def __init__(self,generated_guid = None, bundle_path=None):
+ """
+ Tutorial_bundler constructor. If a GUID is given in the parameter, the
+ Tutorial_bundler object will be associated with it. If no GUID is given,
+ a new GUID will be generated,
+ """
+
+ self.Guid = generated_guid or str(uuid.uuid1())
+
+ #FIXME: Look for the bundle in the activity first (more specific)
+ #Look for the file in the path if a uid is supplied
+ if generated_guid:
+ #General store
+ store_path = os.path.join(_get_store_root(), str(generated_guid), INI_FILENAME)
+ if os.path.isfile(store_path):
+ self.Path = os.path.dirname(store_path)
+ elif _get_bundle_root() != None:
+ #Bundle store
+ bundle_path = os.path.join(_get_bundle_root(), str(generated_guid), INI_FILENAME)
+ if os.path.isfile(bundle_path):
+ self.Path = os.path.dirname(bundle_path)
+ else:
+ raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid)
+ else:
+ raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid)
+
+ else:
+ #Create the folder, any failure will go through to the caller for now
+ store_path = os.path.join(_get_store_root(), self.Guid)
+ os.makedirs(store_path)
+ self.Path = store_path
+
+ def write_metadata_file(self, tutorial):
+ """
+ Write metadata to the property file.
+ @param tutorial Tutorial for which to write metadata
+ """
+ #Create the Config Object and populate it
+ cfg = SafeConfigParser()
+ cfg.add_section(INI_METADATA_SECTION)
+ cfg.set(INI_METADATA_SECTION, INI_GUID_PROPERTY, self.Guid)
+ cfg.set(INI_METADATA_SECTION, INI_NAME_PROPERTY, tutorial.name)
+ cfg.set(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY, TUTORIAL_FILENAME)
+ cfg.add_section(INI_ACTIVITY_SECTION)
+ if os.environ['SUGAR_BUNDLE_NAME'] != None and os.environ['SUGAR_BUNDLE_VERSION'] != None:
+ cfg.set(INI_ACTIVITY_SECTION, os.environ['SUGAR_BUNDLE_NAME'],
+ os.environ['SUGAR_BUNDLE_VERSION'])
+ else:
+ cfg.set(INI_ACTIVITY_SECTION, 'not_an_activity', '0')
+
+ #Write the ini file
+ cfg.write( file( os.path.join(self.Path, INI_FILENAME), 'w' ) )
+
+
+ @staticmethod
+ def get_tutorial_path(guid):
+ """
+ Finds the tutorial with the associated GUID. If it is found, return
+ the path to the tutorial's directory. If it doesn't exist, raise an
+ IOError.
+
+ A note : if there are two tutorials with this GUID in the folders,
+ they will both be inspected and the one with the highest version
+ number will be returned. If they have the same version number, the one
+ from the global store will be returned.
+
+ @param guid The GUID of the tutorial that is to be loaded.
+ """
+ # Attempt to find the tutorial's directory in the global directory
+ global_dir = os.path.join(_get_store_root(),str(guid))
+ # Then in the activty's bundle path
+ if _get_bundle_root() != None:
+ activity_dir = os.path.join(_get_bundle_root(), str(guid))
+ else:
+ activity_dir = ''
+
+ # If they both exist
+ if os.path.isdir(global_dir) and os.path.isdir(activity_dir):
+ # Inspect both metadata files
+ global_meta = os.path.join(global_dir, "meta.ini")
+ activity_meta = os.path.join(activity_dir, "meta.ini")
+
+ # Open both config files
+ global_parser = SafeConfigParser()
+ global_parser.read(global_meta)
+
+ activity_parser = SafeConfigParser()
+ activity_parser.read(activity_meta)
+
+ # Get the version number for each tutorial
+ global_version = global_parser.get(INI_METADATA_SECTION, "version")
+ activity_version = activity_parser.get(INI_METADATA_SECTION, "version")
+
+ # If the global version is higher or equal, we'll take it
+ if global_version >= activity_version:
+ return global_dir
+ else:
+ return activity_dir
+
+ # Do we just have the global directory?
+ if os.path.isdir(global_dir):
+ return global_dir
+
+ # Or just the activity's bundle directory?
+ if os.path.isdir(activity_dir):
+ return activity_dir
+
+ # Error : none of these directories contain the tutorial
+ raise IOError(2, "Neither the global nor the bundle directory contained the tutorial with GUID %s"%guid)
+
+
+ @staticmethod
+ def write_fsm(fsm):
+
+ """
+ Save fsm to disk. If a GUID parameter is provided, the existing GUID is
+ located in the .ini files in the store root and bundle root and
+ the corresponding FSM is/are created or overwritten. If the GUID is not
+ found, an exception occur.
+ """
+
+ config = SafeConfigParser()
+
+ serializer = XMLSerializer()
+ path = os.path.join(self.Path, "meta.ini")
+ config.read(path)
+ xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY)
+ serializer.save_fsm(fsm, xml_filename, self.Path)
+
+ @staticmethod
+ def add_resources(typename, file):
+ """
+ Add ressources to metadata.
+ """
+ raise NotImplementedError("add_resources not implemented")
diff --git a/tutorius/viewer.py b/tutorius/viewer.py
new file mode 100644
index 0000000..751e89a
--- /dev/null
+++ b/tutorius/viewer.py
@@ -0,0 +1,406 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+This module renders a widget containing a graphical representation
+of a tutorial and acts as a creator proxy as it has some editing
+functionality.
+"""
+import sys
+
+import gtk, gtk.gdk
+import cairo
+from math import pi as PI
+PI2 = PI/2
+
+import rsvg
+
+from sugar.bundle import activitybundle
+from sugar.tutorius import addon
+from sugar.graphics import icon
+from sugar.tutorius.filters import EventFilter
+from sugar.tutorius.actions import Action
+import os
+
+# FIXME ideally, apps scale correctly and we should use proportional positions
+X_WIDTH = 800
+X_HEIGHT = 600
+ACTION_WIDTH = 100
+ACTION_HEIGHT = 70
+
+# block look
+BLOCK_PADDING = 5
+BLOCK_WIDTH = 100
+BLOCK_CORNERS = 10
+BLOCK_INNER_PAD = 10
+
+SNAP_WIDTH = BLOCK_WIDTH - BLOCK_PADDING - BLOCK_INNER_PAD*2
+SNAP_HEIGHT = SNAP_WIDTH*X_HEIGHT/X_WIDTH
+SNAP_SCALE = float(SNAP_WIDTH)/X_WIDTH
+
+class Viewer(object):
+ """
+ Renders a tutorial as a sequence of blocks, each block representing either
+ an action or an event (transition).
+
+ Current Viewer implementation lacks viewport management;
+ having many objects in a tutorial will not render properly.
+ """
+ def __init__(self, tutorial, creator):
+ super(Viewer, self).__init__()
+
+ self._tutorial = tutorial
+ self._creator = creator
+ self.alloc = None
+ self.click_pos = None
+ self.drag_pos = None
+ self.selection = []
+
+ self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.win.set_size_request(400, 200)
+ self.win.set_gravity(gtk.gdk.GRAVITY_SOUTH_WEST)
+ self.win.show()
+ self.win.set_deletable(False)
+ self.win.move(0, 0)
+
+ #vbox = gtk.VBox()
+ vbox = gtk.ScrolledWindow()
+ self.win.add(vbox)
+
+ canvas = gtk.DrawingArea()
+ #vbox.pack_start(canvas)
+ vbox.add_with_viewport(canvas) # temp
+ canvas.set_app_paintable(True)
+ canvas.connect_after("expose-event", self.on_viewer_expose, tutorial._states)
+ canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK \
+ |gtk.gdk.BUTTON_MOTION_MASK \
+ |gtk.gdk.BUTTON_RELEASE_MASK \
+ |gtk.gdk.KEY_PRESS_MASK)
+ canvas.connect('button-press-event', self._on_click)
+ # drag-select disabled, for now
+ #canvas.connect('motion-notify-event', self._on_drag)
+ canvas.connect('button-release-event', self._on_drag_end)
+ canvas.connect('key-press-event', self._on_key_press)
+
+ canvas.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS)
+ canvas.grab_focus()
+
+ #self.scroll = gtk.HScrollbar()
+ #vbox.pack_end(self.scroll, False)
+
+ self.win.show_all()
+ canvas.set_size_request(2048, 180) # FIXME
+
+ def destroy(self):
+ self.win.destroy()
+
+
+ def _paint_state(self, ctx, states):
+ """
+ Paints a tutorius fsm state in a cairo context.
+ Final context state will be shifted by the size of the graphics.
+ """
+ block_width = BLOCK_WIDTH - BLOCK_PADDING
+ block_max_height = self.alloc.height
+
+ new_insert_point = None
+ cur_state = 'INIT'
+
+ # FIXME: get app when we have a model that supports it
+ cur_app = 'Calculate'
+ app_start = ctx.get_matrix()
+ try:
+ state = states[cur_state]
+ except KeyError:
+ state = None
+
+ while state:
+ new_app = 'Calculate'
+ if new_app != cur_app:
+ ctx.save()
+ ctx.set_matrix(app_start)
+ self._render_app_hints(ctx, cur_app)
+ ctx.restore()
+ app_start = ctx.get_matrix()
+ ctx.translate(BLOCK_PADDING, 0)
+ cur_app = new_app
+
+ action_list = state.get_action_list()
+ if action_list:
+ local_height = (block_max_height - BLOCK_PADDING)/len(action_list) - BLOCK_PADDING
+ ctx.save()
+ for action in action_list:
+ origin = tuple(ctx.get_matrix())[-2:]
+ if self.click_pos and \
+ self.click_pos[0]-BLOCK_WIDTH<origin[0] and \
+ self.drag_pos[0]>origin[0]:
+ self.selection.append(action)
+ self.render_action(ctx, block_width, local_height, action)
+ ctx.translate(0, local_height+BLOCK_PADDING)
+
+ ctx.restore()
+ ctx.translate(BLOCK_WIDTH, 0)
+
+ # insertion cursor painting made from two opposed triangles
+ # joined by a line.
+ if state.name == self._creator.get_insertion_point():
+ ctx.save()
+ bp2 = BLOCK_PADDING/2
+ ctx.move_to(-bp2, 0)
+ ctx.line_to(-BLOCK_PADDING-bp2, -BLOCK_PADDING)
+ ctx.line_to(bp2, -BLOCK_PADDING)
+ ctx.line_to(-bp2, 0)
+
+ ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING)
+ ctx.line_to(bp2, block_max_height-BLOCK_PADDING)
+ ctx.line_to(-BLOCK_PADDING-bp2, block_max_height-BLOCK_PADDING)
+ ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING)
+
+ ctx.line_to(-bp2, BLOCK_PADDING)
+ ctx.set_source_rgb(1.0, 1.0, 0.0)
+ ctx.stroke_preserve()
+ ctx.fill()
+ ctx.restore()
+
+
+ event_list = state.get_event_filter_list()
+ if event_list:
+ local_height = (block_max_height - BLOCK_PADDING)/len(event_list) - BLOCK_PADDING
+ ctx.save()
+ for event, next_state in event_list:
+ origin = tuple(ctx.get_matrix())[-2:]
+ if self.click_pos and \
+ self.click_pos[0]-BLOCK_WIDTH<origin[0] and \
+ self.drag_pos[0]>origin[0]:
+ self.selection.append(event)
+ self.render_event(ctx, block_width, local_height, event)
+ ctx.translate(0, local_height+BLOCK_PADDING)
+
+ ctx.restore()
+ ctx.translate(BLOCK_WIDTH, 0)
+
+ # FIXME point to next state in state, as it would highlight
+ # the "happy path".
+ cur_state = event_list[0][1]
+
+ if (not new_insert_point) and self.click_pos:
+ origin = tuple(ctx.get_matrix())[-2:]
+ if self.click_pos[0]<origin[0]:
+ new_insert_point = state
+
+ if event_list:
+ try:
+ state = states[cur_state]
+ except KeyError:
+ break
+ yield True
+ else:
+ break
+
+ ctx.set_matrix(app_start)
+ self._render_app_hints(ctx, cur_app)
+
+ if self.click_pos:
+ if not new_insert_point:
+ new_insert_point = state
+
+ self._creator.set_insertion_point(new_insert_point.name)
+
+ yield False
+
+ def _render_snapshot(self, ctx, elem):
+ ctx.set_source_rgba(1.0, 1.0, 1.0, 0.5)
+ ctx.rectangle(0, 0, SNAP_WIDTH, SNAP_HEIGHT)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ if hasattr(elem, 'position'):
+ pos = elem.position
+ # FIXME this size approximation is fine, but I believe we could
+ # do better.
+ ctx.scale(SNAP_SCALE, SNAP_SCALE)
+ ctx.rectangle(pos[0], pos[1], ACTION_WIDTH, ACTION_HEIGHT)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ def _render_app_hints(self, ctx, appname):
+ ctx.set_source_rgb(0.0, 0.0, 0.0)
+ ctx.set_dash((1,1,0,0), 1)
+ ctx.move_to(0, 0)
+ ctx.line_to(0, self.alloc.height)
+ ctx.stroke()
+ ctx.set_dash(tuple(), 1)
+
+ bundle_path = os.getenv("SUGAR_BUNDLE_PATH")
+ if bundle_path:
+ icon_path = activitybundle.ActivityBundle(bundle_path).get_icon()
+ icon = rsvg.Handle(icon_path)
+ ctx.save()
+ ctx.translate(-15, 0)
+ ctx.scale(0.5, 0.5)
+ icon_surf = icon.render_cairo(ctx)
+ ctx.restore()
+
+
+ def render_action(self, ctx, width, height, action):
+ ctx.save()
+ inner_width = width-(BLOCK_CORNERS<<1)
+ inner_height = height-(BLOCK_CORNERS<<1)
+
+ paint_border = ctx.rel_line_to
+ filling = cairo.LinearGradient(0, 0, 0, inner_height)
+ if action not in self.selection:
+ filling.add_color_stop_rgb(0.0, 0.7, 0.7, 1.0)
+ filling.add_color_stop_rgb(1.0, 0.1, 0.1, 0.8)
+ else:
+ filling.add_color_stop_rgb(0.0, 0.4, 0.4, 0.8)
+ filling.add_color_stop_rgb(1.0, 0.0, 0.0, 0.5)
+ tracing = cairo.LinearGradient(0, 0, 0, inner_height)
+ tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0)
+ tracing.add_color_stop_rgb(1.0, 0.2, 0.2, 0.2)
+
+ ctx.move_to(BLOCK_CORNERS, 0)
+ paint_border(inner_width, 0)
+ ctx.arc(inner_width+BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI2, 0.0)
+ ctx.arc(inner_width+BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, 0.0, PI2)
+ ctx.arc(BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, PI2, PI)
+ ctx.arc(BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI, -PI2)
+
+ ctx.set_source(tracing)
+ ctx.stroke_preserve()
+ ctx.set_source(filling)
+ ctx.fill()
+
+ addon_name = addon.get_name_from_type(type(action))
+ # TODO use icon pool
+ icon_name = addon.get_addon_meta(addon_name)['icon']
+ rsvg_icon = rsvg.Handle(icon.get_icon_file_name(icon_name))
+ ctx.save()
+ ctx.translate(BLOCK_INNER_PAD, BLOCK_INNER_PAD)
+ ctx.scale(0.5, 0.5)
+ icon_surf = rsvg_icon.render_cairo(ctx)
+
+ ctx.restore()
+
+ ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2)
+ self._render_snapshot(ctx, action)
+
+ ctx.restore()
+
+ def render_event(self, ctx, width, height, event):
+ ctx.save()
+ inner_width = width-(BLOCK_CORNERS<<1)
+ inner_height = height-(BLOCK_CORNERS<<1)
+
+ filling = cairo.LinearGradient(0, 0, 0, inner_height)
+ if event not in self.selection:
+ filling.add_color_stop_rgb(0.0, 1.0, 0.8, 0.6)
+ filling.add_color_stop_rgb(1.0, 1.0, 0.6, 0.2)
+ else:
+ filling.add_color_stop_rgb(0.0, 0.8, 0.6, 0.4)
+ filling.add_color_stop_rgb(1.0, 0.6, 0.4, 0.1)
+ tracing = cairo.LinearGradient(0, 0, 0, inner_height)
+ tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0)
+ tracing.add_color_stop_rgb(1.0, 0.3, 0.3, 0.3)
+
+ ctx.move_to(BLOCK_CORNERS, 0)
+ ctx.rel_line_to(inner_width, 0)
+ ctx.rel_line_to(BLOCK_CORNERS, BLOCK_CORNERS)
+ ctx.rel_line_to(0, inner_height)
+ ctx.rel_line_to(-BLOCK_CORNERS, BLOCK_CORNERS)
+ ctx.rel_line_to(-inner_width, 0)
+ ctx.rel_line_to(-BLOCK_CORNERS, -BLOCK_CORNERS)
+ ctx.rel_line_to(0, -inner_height)
+ ctx.close_path()
+
+ ctx.set_source(tracing)
+ ctx.stroke_preserve()
+ ctx.set_source(filling)
+ ctx.fill()
+
+ addon_name = addon.get_name_from_type(type(event))
+ # TODO use icon pool
+ icon_name = addon.get_addon_meta(addon_name)['icon']
+ rsvg_icon = rsvg.Handle(icon.get_icon_file_name(icon_name))
+ ctx.save()
+ ctx.translate(BLOCK_INNER_PAD, BLOCK_INNER_PAD)
+ ctx.scale(0.5, 0.5)
+ icon_surf = rsvg_icon.render_cairo(ctx)
+
+ ctx.restore()
+
+ ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2)
+ self._render_snapshot(ctx, event)
+
+ ctx.restore()
+
+ def on_viewer_expose(self, widget, evt, states):
+ ctx = widget.window.cairo_create()
+ self.alloc = widget.get_allocation()
+ ctx.set_source_pixmap(widget.window,
+ widget.allocation.x,
+ widget.allocation.y)
+
+ #draw no more than our expose event intersects our child
+ region = gtk.gdk.region_rectangle(widget.allocation)
+ r = gtk.gdk.region_rectangle(evt.area)
+ region.intersect(r)
+ ctx.region (region)
+ ctx.clip()
+ ctx.paint()
+
+ ctx.translate(BLOCK_PADDING, BLOCK_PADDING)
+
+ painter = self._paint_state(ctx, states)
+ while painter.next(): pass
+
+ if self.click_pos and self.drag_pos:
+ ctx.set_matrix(cairo.Matrix())
+ ctx.rectangle(self.click_pos[0], self.click_pos[1],
+ self.drag_pos[0]-self.click_pos[0],
+ self.drag_pos[1]-self.click_pos[1])
+ ctx.set_source_rgba(0, 0, 1, 0.5)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ return False
+
+ def _on_click(self, widget, evt):
+ # the rendering pipeline will work out the click validation process
+ self.drag_pos = None
+ self.drag_pos = self.click_pos = evt.get_coords()
+ widget.queue_draw()
+
+ self.selection = []
+
+ def _on_drag(self, widget, evt):
+ self.drag_pos = evt.get_coords()
+ widget.queue_draw()
+
+ def _on_drag_end(self, widget, evt):
+ self.click_pos = self.drag_pos = None
+ widget.queue_draw()
+
+ def _on_key_press(self, widget, evt):
+ if evt.keyval == gtk.keysyms.BackSpace:
+ # remove selection
+ for selected in self.selection:
+ if isinstance(selected, EventFilter):
+ self._creator.delete_state()
+ else:
+ self._creator.delete_action(selected)
+ widget.queue_draw()
+
+