From dbf88e0f12fe015467ecf89d95a2ba4303d9f73a Mon Sep 17 00:00:00 2001 From: erick Date: Sat, 05 Dec 2009 00:19:19 +0000 Subject: Merge branch 'master' into frame_integration --- 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/Rating.py b/Workshop.activity/Rating.py new file mode 100644 index 0000000..a13e5a2 --- /dev/null +++ b/Workshop.activity/Rating.py @@ -0,0 +1,144 @@ +import gtk +from gtk import gdk +import logging + +class Rating(gtk.Widget): + """ + Controls that display the rating of a tutorial using colored stars + """ + def __init__(self,tutorial,controller, rating=0,editable = False): + """ + Constructor + + @param the controller to link the view with + @param tutorial The tutorial for which this rating is + @param rating The rating to show + @param editable True if the rating may be edited + """ + gtk.Widget.__init__(self) + + self.tutorial = tutorial + self.controller = controller + self.editable = editable + self.rating = rating + + #star size is 24 pixels by 24 pixels + self.image_length = 24 + + def do_realize(self): + self.set_flags(self.flags() | gtk.REALIZED) + + self.window = gtk.gdk.Window( + self.get_parent_window(), + width=self.allocation.width, + height=self.allocation.height, + window_type=gdk.WINDOW_CHILD, + wclass=gdk.INPUT_OUTPUT, + event_mask=self.get_events() | gtk.gdk.EXPOSURE_MASK + | gtk.gdk.BUTTON_PRESS_MASK) + + self.window.set_user_data(self) + + self.style.attach(self.window) + + self.style.set_background(self.window, gtk.STATE_NORMAL) + self.window.move_resize(*self.allocation) + + #load the stars + pixbuf = gtk.gdk.pixbuf_new_from_file('full_star.png') + self.full_star,mask = pixbuf.render_pixmap_and_mask() + + pixbuf = gtk.gdk.pixbuf_new_from_file('half_star.png') + self.half_star,mask = pixbuf.render_pixmap_and_mask() + + image = gtk.Image() + pixbuf = gtk.gdk.pixbuf_new_from_file('grayed_star.png') + self.empty_star,mask =pixbuf.render_pixmap_and_mask() + + self.gc = self.style.fg_gc[gtk.STATE_NORMAL] + + def do_unrealize(self): + self.window.destroy() + + def do_size_request(self, requisition): + requisition.height = self.image_length + requisition.width = (self.image_length * 5) + + def do_size_allocate(self, allocation): + self.allocation = allocation + if self.flags() & gtk.REALIZED: + self.window.move_resize(*allocation) + + def do_expose_event(self, event): + """ + The widget is drawn here + """ + value = self.rating + stars = [0,0,0,0,0] + if value > 0: + for x in range(5): + if value -1 > 0: + stars[x]=1 + elif value -1 == -0.5: + stars[x] = 0.5 + break + else: + stars[x]=1 + break + value -= 1 + + for x in range(0,5): + if stars[x] == 0: + self.window.draw_drawable(self.gc, self.empty_star, 0, 0 + , x*self.image_length + , 0,-1, -1) + elif stars[x] == 0.5: + self.window.draw_drawable(self.gc, self.half_star, 0, 0 + , x*self.image_length + , 0,-1, -1) + elif stars[x] == 1: + self.window.draw_drawable(self.gc, self.full_star, 0, 0 + , x*self.image_length + , 0,-1, -1) + + def do_button_press_event(self, event): + """When the button is pressed""" + + # make sure it was the first button + if self.editable: + if event.button == 1: + #check for new stars + self.check_for_new_stars(event.x) + + return True + + def check_for_new_stars(self, xPos): + """ + Computes the star number based on where the click was + """ + + new_stars = int(xPos / self.image_length) + half_star = xPos % self.image_length + + logging.info("xpos: %d, new_stars: %d, half_star: %d",xPos,new_stars,half_star) + if half_star > self.image_length/2: + new_stars +=1 + else: + new_stars = new_stars+0.5 + logging.info("rating: %f",new_stars) + self.controller.rate_tutorial(self.tutorial,new_stars) + + self.set_value(new_stars) + + def set_value(self, value): + """ + Sets the value and force a redraw + """ + + if (value >= 0): + self.rating = value + #check for the maximum + if (self.rating > 5): + self.rating = 5 + # redraw the widget + self.queue_draw() \ No newline at end of file diff --git a/Workshop.activity/TutorialStoreCategories.py b/Workshop.activity/TutorialStoreCategories.py new file mode 100644 index 0000000..c321d66 --- /dev/null +++ b/Workshop.activity/TutorialStoreCategories.py @@ -0,0 +1,24 @@ +import sys, os +import gtk + +class TutorialStoreCategories(gtk.Frame): + + def __init__(self,controller): + gtk.Frame.__init__(self,'Categories') + self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0)) + + self.controller = controller + self.categorie_box = gtk.VBox(False, 5) + + self.add(self.categorie_box) + self.categorie_box.show() + + def set_categories(self,categories): + for child in self.categorie_box.get_children(): + self.categorie_box.remove(child) + + for category in categories: + link = gtk.LinkButton("",category) + self.categorie_box.pack_start(link,False,False,10) + link.connect('clicked',self.controller.get_tutorials_by_category,category) + link.show() diff --git a/Workshop.activity/TutorialStoreDetails.py b/Workshop.activity/TutorialStoreDetails.py new file mode 100644 index 0000000..83c5366 --- /dev/null +++ b/Workshop.activity/TutorialStoreDetails.py @@ -0,0 +1,26 @@ +import sys, os +import gtk +from Workshop import WorkshopDetail + +class TutorialStoreDetails(WorkshopDetail): + + def __init__(self,tutorial,controller): + WorkshopDetail.__init__(self,tutorial,controller) + + download_button = gtk.Button('Download') + infos_button = gtk.Button('Informations') + #comp_button = gtk.Button('Compatibility') + + last_row = gtk.HBox(False, 15) + last_row.pack_start(download_button,False,False) + last_row.pack_start(infos_button,False) + #last_row.pack_start(comp_button,False) + + + self.pack_end(last_row,False,False) + + last_row.show_all() + + download_button.connect('clicked',self.controller.download_tutorial,tutorial) + infos_button.connect('clicked',self.controller.display_infos,tutorial) + self.back_button.connect('clicked',self.controller.back_pressed,None) diff --git a/Workshop.activity/TutorialStoreHome.py b/Workshop.activity/TutorialStoreHome.py new file mode 100644 index 0000000..a9051e7 --- /dev/null +++ b/Workshop.activity/TutorialStoreHome.py @@ -0,0 +1,187 @@ +import logging +import TutorialStoreCategories +import TutorialStoreSearch +import TutorialStoreSuggestion +from TutorialStoreDetails import TutorialStoreDetails +from TutorialStoreResults import TutorialStoreResults +from WorkshopController import StoreController +from dialogs import StoreInformationDialog + +import sys, os +import gtk + + +class TutorialStore(gtk.Alignment): + """ + Main container for the Tutorial Store part of the workshop + """ + def __init__(self,model): + gtk.Alignment.__init__(self,0.0,0.0,1.0,1.0) + + self.controller = StoreController(self,model) + model.set_store_view(self) + + self.store_home = TutorialStoreHome(self.controller) + + self.add(self.store_home) + self.store_home.show() + + self.controller.get_categories() + self.controller.get_popular() + self.controller.get_also_like() + + def set_categories(self,categories): + self.store_home.set_categories(categories) + + def set_tutorial_list(self,tutorial_list): + self.store_home.set_tutorial_list(tutorial_list) + + def show_details(self,tutorial): + self.store_home.show_details(tutorial) + + def set_popular(self,tutorials): + self.store_home.set_popular(tutorials) + + def set_also_like(self,tutorials): + self.store_home.set_also_like(tutorials) + + def display_infos(self,tutorial): + self.store_home.display_infos(tutorial) + + def set_button_sensitive(self,back,next): + self.store_home.set_button_sensitive(back,next) + + def show_search_result(self): + self.store_home.show_search_result() + +class TutorialStoreHome(gtk.HBox): + def __init__(self,controller): + gtk.HBox.__init__(self,False,5) + self.controller = controller + + self.categories = TutorialStoreCategories.TutorialStoreCategories(self.controller) + + self.search = TutorialStoreSearch.TutorialStoreSearch(self.controller) + + self.suggestion = TutorialStoreSuggestion.TutorialStoreSuggestion(controller) + + self.center_container = gtk.Alignment(0.0,0.0,1.0,1.0) + self.search_result = TutorialStoreResults(self.controller) + + + self.center_container.add(self.suggestion) + + tut_store_home_base = gtk.VBox(False, 5) + tut_store_home_base.pack_start(self.search, False, False,10) + sep = gtk.HSeparator() + tut_store_home_base.pack_start(sep, False, False) + tut_store_home_base.pack_start(self.center_container,True,True) + + + self.pack_start(self.categories, True, True, 5) + self.pack_start(tut_store_home_base, True, True, 5) + #self.pack_start(tut_store_home_base, True, True, 5) + + #tut_store_suggestion.show() + self.categories.show() + tut_store_home_base.show() + self.search.show() + sep.show() + self.center_container.show_all() + self.suggestion.show_all() + + def set_categories(self,categories): + self.categories.set_categories(categories) + self.search.set_categories(categories) + + def display_infos(self,tutorial): + dialog = StoreInformationDialog(tutorial) + dialog.run() + dialog.destroy() + + def set_popular(self,tutorials): + self.suggestion.set_popular(tutorials) + + def set_also_like(self,tutorials): + self.suggestion.set_also_like(tutorials) + + def set_tutorial_list(self,tutorial_list): + self.search_result.set_tutorial_list(tutorial_list) + for child in self.center_container: + self.center_container.remove(child) + + self.center_container.add(self.search_result) + self.search_result.show() + + def show_details(self,tutorial): + self.details = TutorialStoreDetails(tutorial,self.controller) + for child in self.center_container: + self.center_container.remove(child) + + self.center_container.add(self.details) + self.details.show() + + def show_search_result(self): + for child in self.center_container: + self.center_container.remove(child) + + self.center_container.add(self.search_result) + self.search_result.show() + + + 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 + + def set_button_sensitive(self,back,next): + self.search_result.set_button_sensitive(back,next) diff --git a/Workshop.activity/TutorialStoreResults.py b/Workshop.activity/TutorialStoreResults.py new file mode 100644 index 0000000..3a7f78d --- /dev/null +++ b/Workshop.activity/TutorialStoreResults.py @@ -0,0 +1,120 @@ +import sys, os +import gtk +from Workshop import WorkshopListItem +from Rating import Rating +import operator +import logging + + +class TutorialStoreResults(gtk.VBox): + def __init__(self,controller): + """Constructor + + @param controller The controller to attach the view to + """ + gtk.VBox.__init__(self,False,10) + + back_image = gtk.Image() + back_image.set_from_file('arrow_back.png') + self.back_button = gtk.Button("Prev") + self.back_button.set_image(back_image) + + next_image = gtk.Image() + next_image.set_from_file('arrow_next.png') + self.next_button = gtk.Button("Next") + self.next_button.set_image(next_image) + + self.arrow_box = gtk.HBox() + self.arrow_box.pack_start(self.back_button,False,False) + self.arrow_box.pack_end(self.next_button,False,False) + self.back_button.set_sensitive(False) + + self.controller = controller + self.tutorial_list = [] + + #by default tutorials are sorted by name + self.sorting_key = 'Name' + + self.set_border_width(10) + + #create the list item container with a scroll bar if necessary + 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.pack_end(self.arrow_box,False,False) + + self.back_button.connect("clicked",self.controller.prev_page,None) + self.next_button.connect("clicked",self.controller.next_page,None) + + #Show the components + self.list_container.show() + self.main_container.show() + self.arrow_box.show_all() + + def set_button_sensitive(self,back,next): + self.back_button.set_sensitive(back) + self.next_button.set_sensitive(next) + + def change_sorting(self,sorting): + """ + Changes the property by which tutorial are sorted + + @param sorting The property by which tutorials will be sorted + """ + logging.info("Change_sorting was called") + self.sorting_key = sorting + self.sort_tutorial() + + def sort_tutorial(self): + """ + Sorts the tutorials + """ + #if tutorials are sorted by rating they are in the reverse order + self.tutorial_list.sort(key=operator.attrgetter(self.sorting_key.lower())) + self.refresh_tutorial_display() + + def set_tutorial_list(self,tutorial_list): + """ + Set the list of tutorial to display + + @param tutorial_list the tutorial list + """ + self.tutorial_list = tutorial_list + self.sort_tutorial() + + def refresh_tutorial_display(self): + """ + Refresh the tutorial content by deleting every item and recreating them + """ + #delete every tutorial list item + for child in self.list_container.get_children(): + self.list_container.remove(child) + + #Creates and add a new item for every tutorial + for tuto in self.tutorial_list: + item = TutorialStoreListItem(tuto,self.controller) + self.list_container.pack_start(item) + item.show() + if self.tutorial_list[-1] != tuto: + sep = gtk.HSeparator() + self.list_container.pack_start(sep) + sep.show() + +class TutorialStoreListItem(WorkshopListItem): + def __init__(self,tutorial,controller): + WorkshopListItem.__init__(self,tutorial,controller) + + self.last_row = gtk.HBox(False,15) + self.btn_detail = gtk.Button('Details') + self.last_row.pack_end(self.btn_detail,False,False) + + self.table.attach(self.last_row,1,3,2,3,yoptions = 0) + + self.last_row.show_all() + + #connect the buttons + self.btn_detail.connect("clicked",self.controller.show_details,self.tutorial) \ No newline at end of file diff --git a/Workshop.activity/TutorialStoreSearch.py b/Workshop.activity/TutorialStoreSearch.py new file mode 100644 index 0000000..4303a07 --- /dev/null +++ b/Workshop.activity/TutorialStoreSearch.py @@ -0,0 +1,38 @@ +import sys, os +import gtk + +class TutorialStoreSearch(gtk.HBox): + + def __init__(self,controller): + gtk.HBox.__init__(self,False, 5) + self.controller = controller + search_label = gtk.Label('Search :') + self.search_box = gtk.Entry(400) + in_label = gtk.Label('in') + self.search_combobox = gtk.combo_box_new_text() + self.search_button = gtk.Button('Search') + + self.pack_start(search_label, True, True, 5) + self.pack_start(self.search_box, True, True, 5) + self.pack_start(in_label, True, True, 5) + self.pack_start(self.search_combobox, True, True, 5) + self.pack_start(self.search_button, True, True, 5) + + search_label.show() + self.search_box.show() + in_label.show() + self.search_combobox.show() + self.search_button.show() + + self.search_button.connect("clicked",self.controller.search_store,{'keyword':self.search_box, + 'category':self.search_combobox}) + + def set_categories(self,categories): + self.search_combobox.set_active(0) + while self.search_combobox.get_active_text() is not None: + self.search_combobox.remove_text(0) + self.search_combobox.set_active(0) + + for category in categories: + self.search_combobox.append_text(category) + \ No newline at end of file diff --git a/Workshop.activity/TutorialStoreSuggestion.py b/Workshop.activity/TutorialStoreSuggestion.py new file mode 100644 index 0000000..45fc1c3 --- /dev/null +++ b/Workshop.activity/TutorialStoreSuggestion.py @@ -0,0 +1,80 @@ +import sys, os +import gtk + +class TutorialStoreSuggestion(gtk.HBox): + + def __init__(self,controller): + gtk.HBox.__init__(self,homogeneous=True, spacing=5) + self.controller = controller + + self.popular = gtk.VBox(homogeneous=True, spacing=0) + self.popular_frame = gtk.Frame('Most Popular') + + #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) + + self.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.popular_frame.add(self.popular) + self.also_like_frame.add(self.also_like) + + self.popular.show() + self.popular_frame.show() + self.also_like.show() + self.also_like_frame.show() + + self.pack_start(self.popular_frame) + self.pack_start(self.also_like_frame) + + def set_popular(self,tutorials): + for child in self.popular.get_children(): + self.popular.remove(child) + if len(tutorials)> 3: + for x in range(0,3): + item = SuggestionListItem(self.controller,tutorials[x]) + self.popular.pack_start(item) + item.show() + else: + for x in tutorials: + item = SuggestionListItem(self.controller,x) + self.popular.pack_start(item) + item.show() + + def set_also_like(self,tutorials): + for child in self.also_like.get_children(): + self.also_like.remove(child) + if len(tutorials)> 3: + for x in range(0,3): + item = SuggestionListItem(self.controller,tutorials[x]) + self.also_like.pack_start(item) + item.show() + else: + for x in tutorials: + item = SuggestionListItem(self.controller,x) + self.also_like.pack_start(item) + item.show() + +class SuggestionListItem(gtk.Frame): + def __init__(self,controller,tutorial): + gtk.Frame.__init__(self) + + self.container = gtk.HBox() + + self.label = gtk.Label(tutorial.name) + self.icon = gtk.Image() + self.icon.set_from_file('icon.svg') + self.button = gtk.Button("More") + + self.container.pack_start(icon5, expand=True, fill=True, padding=4) + self.container.pack_start(label5, expand=True, fill=True, padding=0) + self.container.pack_start(more_button5, expand=False, fill=False, padding=5) + self.add(self.container) + #tutorial5_frame.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,0,0)) + + self.container.show_all() \ No newline at end of file diff --git a/Workshop.activity/TutoriusActivity.py b/Workshop.activity/TutoriusActivity.py new file mode 100755 index 0000000..f2f3adc --- /dev/null +++ b/Workshop.activity/TutoriusActivity.py @@ -0,0 +1,120 @@ +# 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.activity import activity +from TutorialStoreHome import TutorialStore +from Workshop import WorkshopView +from WorkshopModel import WorkshopModel +import logging + +import sys, os +import gtk +from dialogs import LoginDialog + +class TutoriusActivity(activity.Activity): + + 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): + activity.Activity.__init__(self,handle) + gtk.gdk.threads_init() + toolbox = activity.ActivityToolbox(self) + self.set_toolbox(toolbox) + toolbox.show() + + self.table = gtk.VPaned() + self.table.set_position(500) + self.left_container = gtk.HBox() + btn1 = gtk.Button("My tutorials") + btn2 = gtk.Button("Tutorial Store") + + self.left_container.pack_start(btn1,expand=False) + self.left_container.pack_start(btn2,expand=False) + + #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.model = WorkshopModel() + + self.workshop_my_tutorial = WorkshopView(self.model) + self.workshop_store = TutorialStore(self.model) + self.model.set_workshop_view(self.workshop_my_tutorial) + self.model.set_store_view(self.workshop_store) + + self.model.query(None) + + self.table.add2(self.left_container) + self.table.add1(self.workshop_my_tutorial) + self.set_canvas(self.table) + + self.workshop_store.show() + self.workshop_my_tutorial.show() + self.table.show_all() + + + btn1.connect("clicked",self.display_my_tutorial,None) + btn2.connect("clicked",self.display_store,None) + + def display_store(self,widget,data): + """ + Display the Tutorial store view + + @param widget The widget that made the call + @param data parameter not used + """ + if self.table.get_child1() == self.workshop_my_tutorial: + self.table.remove(self.workshop_my_tutorial) + if self.table.get_child1() is None: + logging.info("here store") + self.table.add1(self.workshop_store) + + def display_my_tutorial(self,widget,data): + """ + Display the My Tutorial view + + @param widget The widget that made the call + @param data parameter not used + """ + if self.table.get_child1() == self.workshop_store: + self.table.remove(self.workshop_store) + if self.table.get_child1() is None: + logging.info("here my") + self.table.add1(self.workshop_my_tutorial) diff --git a/Workshop.activity/Workshop.py b/Workshop.activity/Workshop.py new file mode 100644 index 0000000..857bf8c --- /dev/null +++ b/Workshop.activity/Workshop.py @@ -0,0 +1,479 @@ +# Copyright (C) 2009, Tutorius.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import gtk +import gobject + +from Rating import Rating +from WorkshopModel import WorkshopModel +from WorkshopController import WorkshopController +from dialogs import InfoDialog +import operator +import logging + +class WorkshopView(gtk.Alignment): + """ + Main container for the Workshop Mytutorial Part + """ + def __init__(self,model): + """ + Constructor + """ + gtk.Alignment.__init__(self,0.0,0.0,1.0,1.0) + + #Register Rating as a gobject + gobject.type_register(Rating) + + #create the model and the controller + self.controller = WorkshopController(self,model) + + #Create the main view + self.mainView = WorkshopMain(self.controller) + self.detailView = None + + #display the main view + self.add(self.mainView) + self.mainView.show() + + + + def set_categories(self,categories): + self.categories = categories + + 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 display_detail(self,tutorial): + """ + Displays the detail view of a tutorial + + @param tutorial the tutorial to display + """ + #hide the main view + self.mainView.hide() + self.remove(self.mainView) + + #create the detail view and show it + self.detailView = MyTutorialDetail(tutorial,self.controller) + self.add(self.detailView) + self.detailView.show() + + def display_main_view(self): + """ + Displays the main view of the Workshop + """ + #hide the detail view + self.detailView.hide() + self.remove(self.detailView) + + #display the main view + 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 + """ + infoDialog = InfoDialog(tutorial,self.controller,self.categories) + infoDialog.run() + infoDialog.destroy() + + + def refresh_content(self): + """ + Refresh the data displayed + """ + #refresh the tutorial list + self.mainView.refresh_tutorial_display() + + #refresh the detail view + if self.detailView is not None: + self.detailView.refresh_content() + +class WorkshopMain(gtk.VBox): + """ + Contains the main view for the Workshop My tutorial + """ + def __init__(self,controller): + """Constructor + + @param controller The controller to attach the view to + """ + gtk.VBox.__init__(self,False,10) + + self.controller = controller + self.tutorial_list = [] + + #by default tutorials are sorted by name + self.sorting_key = 'Name' + + self.set_border_width(10) + + #The searchbar is displayed at the top + self.search_bar = SearchBar(self.controller) + self.pack_start(self.search_bar,False,False) + + #Add a separator after the search bar + sep = gtk.HSeparator() + self.pack_start(sep,False,False) + + #create the list item container with a scroll bar if necessary + 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) + + #Show the components + self.search_bar.show() + self.list_container.show() + self.main_container.show() + sep.show() + + + def change_sorting(self,sorting): + """ + Changes the property by which tutorial are sorted + + @param sorting The property by which tutorials will be sorted + """ + + self.sorting_key = sorting + self.sort_tutorial() + + def sort_tutorial(self): + """ + Sorts the tutorials + """ + + #if tutorials are sorted by rating they are in the reverse order + self.tutorial_list.sort(key=operator.attrgetter(self.sorting_key.lower())) + self.refresh_tutorial_display() + + def set_tutorial_list(self,tutorial_list): + """ + Set the list of tutorial to display + + @param tutorial_list the tutorial list + """ + self.tutorial_list = tutorial_list + self.sort_tutorial() + + def refresh_tutorial_display(self): + """ + Refresh the tutorial content by deleting every item and recreating them + """ + #delete every tutorial list item + for child in self.list_container.get_children(): + self.list_container.remove(child) + + #Creates and add a new item for every tutorial + for tuto in self.tutorial_list: + item = MyTutorialListItem(tuto,self.controller) + self.list_container.pack_start(item) + item.show() + if self.tutorial_list[-1] != tuto: + sep = gtk.HSeparator() + self.list_container.pack_start(sep) + sep.show() + +class SearchBar(gtk.HBox): + """ + The search bar control for the Workshop My tutorial + """ + def __init__(self,controller): + """ + Constructor + + @param controller The controller to link the view to + """ + gtk.HBox.__init__(self,False,10) + + self.set_border_width(5) + self.controller = controller + + #creating and configuring the controls + 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() + + #Adding the controls to the view + 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,) + + #showing the controls + self.search_entry.show() + self.search_button.show() + self.separator.show() + self.sort_label.show() + self.sort_combo.show() + + #connecting the events + self.search_button.connect("clicked",self.controller.tutorial_query,self.search_entry) + self.sort_combo.connect("changed",self.controller.sort_selection_changed,None) + +class WorkshopDetail(gtk.VBox): + def __init__(self,tutorial,controller): + """ + Constructor + + @param tutorial The tutorial to display + @param controller The controller to link the view with + """ + + #Used for string formatting + self.title_text = '%(title)s' + self.author_text = 'by %(author)s' + self.desc_text = 'Description: %(description)s' + + self.controller = controller + self.tutorial = tutorial + + gtk.VBox.__init__(self,False,10) + self.set_border_width(10) + + #The first row contains the back button + 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) + + #The second row contains the activity icon, the title label, + #the author label and the star rating + icon = gtk.Image() + icon.set_from_file('icon.svg') + + label_holder = gtk.VBox(False,10) + + self.title_label = gtk.Label("") + self.author_label = gtk.Label("") + + #Add a small offsert for author's label alignement because it's cute + self.author_label.set_alignment(0.05,0.5) + self.title_label.set_alignment(0.0,0.5) + + label_holder.pack_start(self.title_label) + label_holder.pack_start(self.author_label) + + self.rating = Rating(tutorial,controller,rating = tutorial.rating) + + second_row = gtk.HBox(False) + second_row.pack_start(icon,False,False) + second_row.pack_start(label_holder) + second_row.pack_end(self.rating,False,False) + + #The middle of the screen contains an area for the description + 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") ) + + #The description view contains all the extra space + self.pack_start(first_row,False,False) + self.pack_start(second_row,False,False) + self.pack_start(self.desc_view) + + #show everything + 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() + + #set some text with markup + self.title_label.set_markup(self.title_text % {"title":tutorial.name}) + self.author_label.set_markup(self.author_text % {"author":tutorial.author}) + + def realize_cb(self,widget,data=None): + """ + This fucntion changes the cursor over the description view + So we see an arrow and not the insert text cursor + """ + widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)) + + def refresh_content(self): + """ + Refresh the labels' text based on the tutorial object + """ + self.title_label.set_markup(self.title_text % {"title":self.tutorial.name}) + self.author_label.set_markup(self.author_text % {"author":self.tutorial.author}) + self.desc_buff.set_text(self.tutorial.description) + +class WorkshopListItem(gtk.Alignment): + """ + A list item containing the details of a tutorial + """ + def __init__(self,tutorial,controller): + """ + Constructor + + @param controller The controller to link the view to + """ + #logging.info(tutorial.updated_metadata) + gtk.Alignment.__init__(self,0.0,0.0,1.0,1.0) + self.tutorial = tutorial + self.controller = controller + self.set_border_width(10) + + #The table will contain everything else + self.table = gtk.Table(3,3,False) + self.table.set_row_spacing(1,10) + + #Create the controls + self.lbl_title = gtk.Label('') + self.lbl_title.set_alignment(0.0,0.5) + self.title_text = '%(title)s' + self.lbl_title.set_markup(self.title_text % {'title':tutorial.name}) + + self.lbl_desc = gtk.Label(tutorial.description) + self.lbl_desc.set_line_wrap(True) + self.lbl_desc.set_alignment(0.0,0.5) + + self.icon = gtk.Image() + self.icon.set_from_file('icon.svg') + + self.rating = Rating(tutorial,controller,tutorial.rating, True) + + #Add the controls to the table + 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(self.rating,2,3,0,2,0,0) + + #show everything + self.table.show() + self.icon.show() + self.lbl_title.show() + self.lbl_desc.show() + self.rating.show() + + self.add(self.table) + + +class MyTutorialListItem(WorkshopListItem): + def __init__(self,tutorial,controller): + WorkshopListItem.__init__(self,tutorial,controller) + + self.last_row = gtk.HBox(False,15) + self.btn_launch = gtk.Button('Launch') + self.btn_detail = gtk.Button('Details') + self.last_row.pack_start(self.btn_launch,False,False) + self.last_row.pack_end(self.btn_detail,False,False) + + self.table.attach(self.last_row,1,3,2,3,yoptions = 0) + + self.last_row.show_all() + + #connect the buttons + self.btn_detail.connect("clicked",self.controller.show_details,self.tutorial) + self.btn_launch.connect("clicked",self.controller.launch_tutorial,self.tutorial) + + +class MyTutorialDetail(WorkshopDetail): + def __init__(self,tutorial,controller): + WorkshopDetail.__init__(self,tutorial,controller) + + #The bottom of the screen contains the button(fourth and fifth row + 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 = gtk.HBox(False,15) + 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) + + + 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 = gtk.HBox(False,15) + fifth_row.pack_start(self.publish_button,False,False) + fifth_row.pack_start(self.unpublish_button,False,False) + + self.pack_end(fifth_row,False,False) + self.pack_end(fourth_row,False,False) + + fifth_row.show_all() + fourth_row.show_all() + + #connect the clicked events of the buttons + self.back_button.connect("clicked",self.controller.back_pressed,None) + self.publish_button.connect("clicked",self.controller.publish_tutorial,self.tutorial) + self.unpublish_button.connect("clicked",self.controller.unpublish_tutorial,self.tutorial) + self.launch_button.connect("clicked",self.controller.launch_tutorial,self.tutorial) + self.edit_button.connect("clicked",self.controller.edit_tutorial,self.tutorial) + self.update_button.connect("clicked",self.controller.update_tutorial,self.tutorial) + self.info_button.connect("clicked",self.controller.info_tutorial,self.tutorial) + self.delete_button.connect("clicked",self.controller.delete_tutorial,self.tutorial) + + self.controller.get_categories() + + diff --git a/Workshop.activity/WorkshopController.py b/Workshop.activity/WorkshopController.py new file mode 100644 index 0000000..84b5999 --- /dev/null +++ b/Workshop.activity/WorkshopController.py @@ -0,0 +1,201 @@ +""" +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.get_text()]) + + 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(self,widget,tutorial): + """ + Handles start tutorial action + + @param widget the widget that triggered the action + @param tutorial the tutorial to launch + """ + self.model.launch_tutorial(tutorial) + + 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,tutorial,rating): + """ + Change the rating for a tutorial + + @param tutorial The tutorial to rate + @param rating The new rating + """ + self.model.rate_tutorial(tutorial,rating) + + 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 + """ + self.model.edit_tutorial(tutorial) + + 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 + """ + self.view.display_info_dialog(tutorial) + + def save_tutorial_info(self,tutorial): + """ + Save the metadata of a tutorial + + @param tutorial The tutorial to update containing the new metadata + """ + self.model.save_metadata(tutorial) + + 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) + + + def get_categories(self): + self.model.get_categories_for_workshop() + +class StoreController(): + def __init__(self,view,model): + self.view = view + self.model = model + self.last_call_search = None + self.last_call_keyword = None + self.last_call_cat = None + + def show_details(self,widget,tutorial): + self.view.show_details(tutorial) + + def get_categories(self): + self.last_call_search = False + self.model.get_categories() + + def search_store(self,widget,data): + cat = data["category"].get_active_text() + keyword = data["keyword"].get_text() + if cat is None or cat == "": + cat = 'all' + self.model.search_store(keyword,cat) + self.last_call_search = True + self.last_call_keyword = keyword + self.last_call_cat = cat + self.last_call_page = 1 + + def get_tutorials_by_category(self,widget,category): + self.model.get_tutorials_by_category(category) + self.last_call_search = False + self.last_call_cat = category + self.last_call_page = 1 + + def download_tutorial(self,widget,tutorial): + self.model.download_tutorial(tutorial) + + def get_also_like(self): + self.model.get_also_like() + + def get_popular(self): + self.model.get_popular() + + def display_infos(self,widget,tutorial): + self.view.display_infos(tutorial) + + def next_page(self,widget,data): + self.last_call_page = self.last_call_page + 1 + if self.last_call_search: + self.model.search_store(self.last_call_keyword,self.last_call_cat,page = self.last_call_page ) + else: + self.model.get_tutorials_by_category(self.last_call_cat,page = self.last_call_page ) + + def prev_page(self,widget,data): + self.last_call_page = self.last_call_page - 1 + if self.last_call_search: + self.model.search_store(self.last_call_keyword,self.last_call_cat,page = self.last_call_page ) + else: + self.model.get_tutorials_by_category(self.last_call_cat,page = self.last_call_page ) + + def back_pressed(self,widget,data): + self.view.show_search_result() + + def rate_tutorial(self,tutorial,rating): + """ + Change the rating for a tutorial + + @param tutorial The tutorial to rate + @param rating The new rating + """ + self.model.rate_tutorial(tutorial,rating) diff --git a/Workshop.activity/WorkshopModel.py b/Workshop.activity/WorkshopModel.py new file mode 100644 index 0000000..c52ab5b --- /dev/null +++ b/Workshop.activity/WorkshopModel.py @@ -0,0 +1,503 @@ +# 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 +""" +WorkshopModel + +This module is the model of the Workshop Activity +""" + + +from sugar.tutorius.vault import * +from sugar.tutorius.store import * +from dialogs import LoginDialog,WaitingDialog,ErrorDialog +import gtk +import threading +from copy import deepcopy +import gobject +import sys,traceback +import logging + +class Login(object): + def __init__(self,proxy,login_view): + self.proxy = proxy + self.login_view = login_view + + def __call__(self,f): + def wrapper(*args): + self.model = self.login_view.get_model() + self.login_view = LoginDialog() + self.login_view.set_model(self.model) + + if self.proxy.get_session_id() == None: + result = self.login_view.run() + self.login_view.destroy() + if result == gtk.RESPONSE_ACCEPT: + f(*args) + else: + f(*args) + return wrapper + + +class WorkshopModel(): + + store_proxy = StoreProxy("http://bobthebuilder.mine.nu/tutorius/en-US/tutorius") + login_view = LoginDialog() + + def __init__(self): + self.login_view.set_model(self) + self.store_page_number = 1 + + def set_workshop_view(self,view): + self.workshop_view = view + + def set_store_view(self,view): + self.store_view = view + + def login(self,username,password): + """ + Log a user in the store + + @param username The username of the user + @param password The password of the user + @return True if login succeeded False otherwise + """ + return self.store_proxy.login(username,password) + + def register(self,username,password,email): + """ + Register a new user to the store + + @param username The username of the new user + @param password The password of the user + @param email The email of the user + @return True if user is registered, False otherwise + """ + return self.store_proxy.register_new_user({'nickname':username,'password':password,'email':email}) + + 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 + """ + if keyword is None or keyword == []: + vault_return = Vault.query() + else: + vault_return = Vault.query(keyword=keyword) + + tutorial_list = [] + for tuto in vault_return: + tutorial_list.append(Tutorial(tuto)) + + self.workshop_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 + """ + Vault.deleteTutorial(tutorial.id) + self.query(None) + self.workshop_view.display_main_view() + + def update_tutorial_infos(self,tutorial, new_infos): + """ + Updates the metadata on a tutorial and updates the currently managed tutorials + Notifies the view that 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"} + """ + Vault.update_metadata(tutorial.id, tutorial.updated_metadata) + + @Login(store_proxy,login_view) + def publish_tutorial(self,tutorial): + """ + Publishes a tutorial + + Details to come + """ + + archive = Vault.get_tutorial_archive(tutorial.id) + metadata = self.tutorial_to_store_metadata(tutorial) + if not tutorial.published_state: + + remote_id = self.store_proxy.publish(archive,metadata) + else: + remote_id = self.store_proxy.update_published_tutorial(archive,metadata,tutorial.remote_id) + + + if remote_id == -1: + dialog = ErrorDialog("An error occured while publishing the tutorial") + dialog.run() + dialog.destroy() + return + + tutorial.remote_id = remote_id + tutorial.published_state = True + + metadata = self.tutorial_to_vault_metadata(tutorial) + Vault.update_metadata(tutorial.id,metadata) + + @Login(store_proxy,login_view) + def unpublish_tutorial(self,tutorial): + """ + Unpublishes a tutorial + + @param tutorial The tutorial to unpublish + """ + logging.info(tutorial.remote_id) + self.store_proxy.unpublish(tutorial.remote_id) + + def launch_tutorial(self,tutorial): + """ + Lauches a tutorial + + @param tutorial The tutorial to launch + """ + pass + + @Login(store_proxy,login_view) + def rate_tutorial(self,tutorial,rating): + """ + Rate the tutorial + + @param tutorial The tutorial to rate + @param rating The new rating for the tutorial + """ + tutorial.rating = rating + logging.info(tutorial.updated_metadata) + logging.info(tutorial.remote_id) + if tutorial.remote_id is not None and int(tutorial.remote_id) > 0: + logging.info("this is here") + self.store_proxy.rate(rating,tutorial.remote_id) + self.workshop_view.refresh_content() + + def edit_tutorial(self,tutorial): + """ + Edit a tutorial + + @param tutorial The tutorial to edit + """ + pass + + def save_metadata(self,tutorial): + """ + Save the metadata of a tutorial + + @param tutorial The tutorial to udpate containing the new metadata + """ + metadata = self.tutorial_to_vault_metadata(tutorial) + Vault.update_metadata(tutorial.id,metadata) + self.workshop_view.refresh_content() + + def get_categories_for_workshop(self): + """ + Get all categories for selecting one in the workshop + """ + self.categories = self.store_proxy.get_categories() + result = [] + for category in self.categories: + result.append(category["name"]) + + self.workshop_view.set_categories(result) + + +#Function related to the store + + def get_categories(self): + """ + Get all categories of tutorial from the store + """ + self.categories = self.store_proxy.get_categories() + result = ['all'] + for category in self.categories: + result.append(category["name"]) + + self.store_view.set_categories(result) + + def search_store(self,keyword,category,page=1): + """ + Search the store for specific tutorial + + @param keyword The keyword to search for + @param category The category to search tutorial for + @param page The page result to return + """ + tut_list = self.store_proxy.search(keyword,category,page) + show_next = False + show_prev = True + if page == 1: + show_prev = False + if len(tut_list) == 10: + next_list = self.store_proxy.search(keyword,category,page+1) + if len(next_list) > 0: + show_next = True + tutorials = [] + for tut in tut_list: + tutorials.append(Tutorial(tut)) + self.store_view.set_tutorial_list(tutorials) + self.store_view.set_button_sensitive(show_prev,show_next) + + def get_tutorials_by_category(self,category,page=1): + """ + Get all the tutorial in a specified category + + @param category The category to search + @param The page result to return + """ + for cat in self.categories: + if cat["name"] == category: + category = cat["id"] + break + tut_list = self.store_proxy.get_tutorials(category,page) + show_next = False + show_prev = True + if page == 1: + show_prev = False + if len(tut_list) == 10: + next_list = self.store_proxy.get_tutorials(category,page+1) + if len(next_list) > 0: + show_next = True + tutorials = [] + for tut in tut_list: + tutorials.append(Tutorial(tut)) + self.store_view.set_tutorial_list(tutorials) + self.store_view.set_button_sensitive(show_prev,show_next) + + def download_tutorial(self,tutorial): + """ + Download a tutorial from the store + + @param tutorial The tutorial to download + """ + thread = threading.Thread(target = self.background_download,kwargs={"tutorial":tutorial}) + self.dialog = WaitingDialog(thread.start,{}) + thread.start() + self.dialog.run() + self.dialog.destroy() + + + def background_download(self,tutorial): + try: + downloaded = self.store_proxy.download_tutorial(int(tutorial.remote_id)) + temp_file = open('temp.zip','w') + temp_file.write(downloaded.read()) + temp_file.close() + Vault.installTutorials(os.getcwd(),'temp.zip') + temp_list = Vault.query(str(tutorial.name)) + logging.info(temp_list) + for tut in temp_list: + tuto = Tutorial(tut) + if tutorial.name == tuto.name: + tuto.remote_id = tutorial.remote_id + Vault.update_metadata(tuto.id,tuto.updated_metadata) + except: + traceback.print_exc() + finally: + gobject.idle_add(self.dialog.response,0) + + def get_popular(self): + tutorials = self.store_proxy.list('Popular') + result = [] + for tutorial in tutorials: + result.append(Tutorial(tutorial)) + self.store_view.set_popular(result) + + def get_also_like(self): + tutorials = self.store_proxy.list('Recommended') + result = [] + for tutorial in tutorials: + result.append(Tutorial(tutorial)) + self.store_view.set_also_like(result) + + + + def tutorial_to_store_metadata(self,tutorial): + metadata = {} + metadata['guid'] = tutorial.id + metadata['name'] = tutorial.name + metadata['version'] = str(tutorial.version) + metadata['summary'] = tutorial.description + metadata['homepage'] = "http://www.tutorius.org" + metadata['description'] = tutorial.description + metadata['filename'] = tutorial.name +".zip" + for category in self.categories: + if category["name"] == tutorial.category: + metadata['cat1'] = category["id"] + logging.info(metadata) + return metadata + + def tutorial_to_vault_metadata(self,tutorial): + metadata = {} + for key in tutorial.updated_metadata.keys(): + if key == 'activities': + metadata[key] = tutorial.updated_metadata[key] + else: + metadata[key] = str(tutorial.updated_metadata[key]) + return metadata + +class Tutorial(object): + """ + Wrapper for tutorial metadata + """ + def __init__(self,metadata_dict): + object.__init__(self) + self.__original_dict = metadata_dict + self.__update_dict = {} + for key in self.__original_dict.keys(): + self.__update_dict[key] = self.__original_dict[key] + + if 'name' in self.__original_dict: + self.__name = self.__original_dict['name'] + else: + self.__name = "" + + if 'version' in self.__original_dict: + if self.__original_dict['version'] == '': + self.__version = 0 + else: + self.__version = int(self.__original_dict['version']) + else: + self.__version = 0 + + 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: + if self.__original_dict['rating'] == '': + self.__rating = 0 + else: + self.__rating = float(self.__original_dict['rating']) + else: + self.__rating = 0 + + if 'category' in self.__original_dict: + self.__category = self.__original_dict['category'] + else: + self.__category = "" + + if 'publish_state' in self.__original_dict: + #I'm sorry for this + temp = self.__original_dict['publish_state'] + temp = temp.lower() + if temp == 'false': + self.__published_state = False + elif temp == 'true': + self.__published_state = True + else: + self.__published_state = None + else: + self.__published_state = None + + if 'guid' in self.__original_dict: + self.__id = self.__original_dict['guid'] + else: + self.__id = "" + + if 'id' in self.__original_dict: + self.__remote_id = self.__original_dict['id'] + else: + self.__remote_id = "" + + + def get_name(self): + return self.__name + + def set_name(self,name): + self.__name = name + self.__update_dict['name'] = name + + def get_version(self): + return self.__version + + def set_version(self,version): + self.__version = version + self.__update_dict['version'] = version + + 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_category(self): + return self.__category + + def set_category(self,category): + self.__category = category + self.__update_dict['category'] = category + + def get_published_state(self): + return self.__published_state + + def set_published_state(self,published_state): + self.__published_state = published_state + self.__update_dict['published_state'] = published_state + + def get_id(self): + return self.__id + + def set_id(self,id): + self.__id = id + self.__update_dict['guid'] = id + + def get_remote_id(self): + return self.__remote_id + + def set_remote_id(self,id): + self.__remote_id = id + self.__update_dict['id'] = id + + def get_updated_metadata(self): + return self.__update_dict + + name = property(get_name,set_name) + version = property(get_version, set_version) + description = property(get_description,set_description) + author = property(get_author,set_author) + rating = property(get_rating,set_rating) + category = property(get_category,set_category) + published_state = property(get_published_state,set_published_state) + id = property(get_id,set_id) + remote_id = property(get_remote_id, set_remote_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 @@ + + +]> + + + + + + + + image/svg+xml + + + + + + + + + \ 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/arrow_next.png b/Workshop.activity/arrow_next.png new file mode 100644 index 0000000..888e431 --- /dev/null +++ b/Workshop.activity/arrow_next.png Binary files differ diff --git a/Workshop.activity/dialogs.py b/Workshop.activity/dialogs.py new file mode 100644 index 0000000..3d5f04a --- /dev/null +++ b/Workshop.activity/dialogs.py @@ -0,0 +1,346 @@ +# Copyright (C) 2009, Tutorius.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import gtk +import logging +import threading + +class ErrorDialog(gtk.Dialog): + def __init__(self,text): + gtk.Dialog.__init__(self,"Error",None,gtk.DIALOG_MODAL,(gtk.STOCK_OK,gtk.RESPONSE_ACCEPT)) + self.label = gtk.Label(text) + self.vbox.pack_start(self.label,padding = 10) + self.vbox.show_all() + +class WaitingDialog(gtk.Dialog): + def __init__(self,callback,kwargs): + self.callback = callback + self.kwargs = kwargs + gtk.Dialog.__init__(self,"Please wait...",None,gtk.DIALOG_MODAL) + self.label = gtk.Label("Please wait while getting information from the store") + self.vbox.pack_start(self.label, padding = 10) + self.vbox.show_all() + + def operation_complete(self): + self.label.set_text("Operation complete") + self.ok_button = gtk.Button("Ok") + self.add_button(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT) + + + +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 set_model(self,model): + self.model = model + + def get_model(self): + return self.model + + 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("Email:") + 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): + success = self.model.login(self.username_entry.get_text(), self.password_entry.get_text()) + if success: + self.response(gtk.RESPONSE_ACCEPT) + else: + errorDialog = ErrorDialog("The password or the email address is wrong") + errorDialog.run() + errorDialog.destroy() + + + def click_link(self,widget,data=None): + self.register_dialog = RegisterDialog(self.model) + self.register_dialog.run() + self.register_dialog.destroy() + +class RegisterDialog(gtk.Dialog): + def __init__(self,model): + gtk.Dialog.__init__(self,"Register",None,gtk.DIALOG_MODAL) + self.model = model + 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): + if not self.data_validation(): + return + success = self.model.register(self.entries[2].get_text(),self.entries[3].get_text(),self.entries[0].get_text()) + if success : + self.response(gtk.RESPONSE_ACCEPT) + else: + self.show_error_dialog("An error occured while registering the user") + + def data_validation(self): + username = self.entries[2].get_text() + password = self.entries[3].get_text() + confirmation = self.entries[4].get_text() + email = self.entries[0].get_text() + + if username.trim() == "": + self.show_error_dialog('You must choose a username') + return False + if password.trim() == '': + self.show_error_dialog('You must provide a password') + return False + if email.trim() == '': + self.show_error_dialog('You must provide a valid email address') + return False + if password != confirmation: + self.show_error_dialog('The password and confirmation must be the same') + return False + + return True + + def show_error_dialog(self,error_message): + errorDialog = ErrorDialog("An error occured while registering a user") + errorDialog.run() + errorDialog.destroy() + + def create_content(self): + entry_length = 40 + table = gtk.Table(10,4,False) + self.entries = [] + 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() + self.entries.append(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) + +class InfoDialog(gtk.Dialog): + def __init__(self,tutorial,controller,categories): + gtk.Dialog.__init__(self,"Tutorial Info",None,gtk.DIALOG_MODAL) + self.tutorial = tutorial + self.controller = controller + self.categories = categories + ok_button = gtk.Button("Save") + 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_content() + + def prepare_content(self): + table = gtk.Table(4,2,False) + labels = ["Tutorial Name","Version","Description","Category"] + entry_length = [40,40,40] + self.entries = [] + + self.name_entry = gtk.Entry() + self.name_entry.set_width_chars(40) + self.name_entry.set_text(self.tutorial.name) + table.attach(self.name_entry,1,2,0,1,ypadding=10) + + + self.version_entry = gtk.Entry() + self.version_entry.set_width_chars(40) + self.version_entry.set_text(str(self.tutorial.version)) + table.attach(self.version_entry,1,2,1,2,ypadding=10) + + self.desc_entry = gtk.Entry() + self.desc_entry.set_width_chars(40) + self.desc_entry.set_text(self.tutorial.description) + table.attach(self.desc_entry,1,2,2,3,ypadding=10) + + self.category_entry = gtk.combo_box_new_text() + logging.info(len(self.categories)) + logging.info(self.categories) + for cat in range(0,len(self.categories)): + self.category_entry.append_text(self.categories[cat]) + logging.info(self.tutorial.category) + logging.info(self.categories[cat]) + if self.tutorial.category != '' and self.tutorial.category == str(self.categories[cat]): + self.category_entry.set_active(cat) + table.attach(self.category_entry,1,2,3,4,ypadding=10) + + for x in range(0,4): + label = gtk.Label(labels[x]) + table.attach(label,0,1,x,x+1,xpadding=10) + label.set_alignment(0.0,0.5) + table.show_all() + self.vbox.pack_start(table) + + def ok_clicked(self,widget,data): + self.tutorial.version = int(self.version_entry.get_text()) + self.tutorial.name = self.name_entry.get_text() + self.tutorial.description = self.desc_entry.get_text() + self.tutorial.category = self.category_entry.get_active_text() + + self.controller.save_tutorial_info(self.tutorial) + self.response(gtk.RESPONSE_ACCEPT) + +class StoreInformationDialog(gtk.Dialog): + def __init__(self,tutorial): + self.text = ' %(text)s: ' + self.value = ' %(text)s ' + + gtk.Dialog.__init__(self,"Tutorial Information",None,gtk.DIALOG_MODAL,(gtk.STOCK_OK,gtk.RESPONSE_ACCEPT)) + + self.table = gtk.Table(6,2,False) + self.tut_name = gtk.Label("") + self.tut_name.set_markup(self.text % {"text":"Name"}) + self.tut_name_value = gtk.Label() + self.tut_name_value.set_markup(self.value % {"text":tutorial.name}) + + self.author = gtk.Label() + self.author.set_markup(self.text % {"text":"Author"}) + self.author_value = gtk.Label() + self.author_value.set_markup(self.value % {"text":tutorial.author}) + + + self.desc = gtk.Label() + self.desc.set_markup(self.text % {"text":"Description"}) + self.desc_value = gtk.Label() + self.desc_value.set_markup(self.value % {"text":tutorial.description}) + + + self.version = gtk.Label() + self.version_value = gtk.Label() + self.version.set_markup(self.text % {"text":"Version"}) + self.version_value.set_markup(self.value % {"text":str(tutorial.version)}) + + self.rating = gtk.Label() + self.rating_value = gtk.Label() + self.rating.set_markup(self.text % {"text":"Rating"}) + self.rating_value.set_markup(self.value % {"text":str(tutorial.rating)}) + + self.category = gtk.Label() + self.category_value = gtk.Label() + self.category.set_markup(self.text % {"text":"Category"}) + self.category_value.set_markup(self.value % {"text":tutorial.category}) + + self.table.attach(self.tut_name,0,1,0,1,ypadding=5) + self.table.attach(self.tut_name_value,1,2,0,1,ypadding=5) + self.table.attach(self.author,0,1,1,2,ypadding=5) + self.table.attach(self.author_value,1,2,1,2,ypadding=5) + self.table.attach(self.desc,0,1,2,3,ypadding=5) + self.table.attach(self.desc_value,1,2,2,3,ypadding=5) + self.table.attach(self.version,0,1,3,4,ypadding=5) + self.table.attach(self.version_value,1,2,3,4,ypadding=5) + self.table.attach(self.rating,0,1,4,5,ypadding=5) + self.table.attach(self.rating_value,1,2,4,5,ypadding=5) + self.table.attach(self.category,0,1,5,6,ypadding=5) + self.table.attach(self.category_value,1,2,5,6,ypadding=5) + + self.vbox.pack_start(self.table) + self.table.show_all() + 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 @@ + + +]> + + + + + + + + image/svg+xml + + + + + + + + + \ 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 7c3c0fd..53387bf 100644 --- a/addons/bubblemessage.py +++ b/addons/bubblemessage.py @@ -94,7 +94,7 @@ class BubbleMessage(Action): self.overlay.put(self._bubble, x, y) self._bubble.show() - self._drag = DragWrapper(self._bubble, self.position, True) + self._drag = DragWrapper(self._bubble, self.position, update_action_cb=self.update_property, draggable=True) def exit_editmode(self, *args): if self._drag.moved: diff --git a/addons/bubblemessagewimg.py b/addons/bubblemessagewimg.py index 1d5d5d3..514a311 100644 --- a/addons/bubblemessagewimg.py +++ b/addons/bubblemessagewimg.py @@ -97,7 +97,7 @@ class BubbleMessageWImg(Action): self.overlay.put(self._bubble, x, y) self._bubble.show() - self._drag = DragWrapper(self._bubble, self.position, True) + self._drag = DragWrapper(self._bubble, self.position, update_action_cb=self.update_property, draggable=True) def exit_editmode(self, *args): x,y = self._drag.position diff --git a/addons/gtkwidgeteventfilter.py b/addons/gtkwidgeteventfilter.py index b5ce9ae..ac14399 100644 --- a/addons/gtkwidgeteventfilter.py +++ b/addons/gtkwidgeteventfilter.py @@ -65,6 +65,5 @@ __event__ = { "icon" : "player_play", "class" : GtkWidgetEventFilter, "mandatory_props" : ["object_id", "event_name"], - "test" : True } diff --git a/addons/gtkwidgettypefilter.py b/addons/gtkwidgettypefilter.py index 4ffecb5..8faf172 100644 --- a/addons/gtkwidgettypefilter.py +++ b/addons/gtkwidgettypefilter.py @@ -96,5 +96,6 @@ __event__ = { 'display_name' : 'Widget Filter', 'icon' : '', 'class' : GtkWidgetTypeFilter, - 'mandatory_props' : ['next_state', 'object_id'] + 'mandatory_props' : ['next_state', 'object_id'], + "test" : True, } diff --git a/data/ui/creator.glade b/data/ui/creator.glade index 1c9669d..aeba19c 100644 --- a/data/ui/creator.glade +++ b/data/ui/creator.glade @@ -1,16 +1,19 @@ - + + + - - 300 500 + GTK_WINDOW_TOPLEVEL Toolbox False - center-on-parent + False + GTK_WIN_POS_CENTER_ON_PARENT 200 500 True + GDK_WINDOW_TYPE_HINT_UTILITY True True False @@ -19,35 +22,37 @@ True - vertical + GTK_ORIENTATION_VERTICAL 5 + GTK_ORIENTATION_VERTICAL True 5 - start + GTK_BUTTONBOX_START - gtk-save True True True + gtk-save True + 0 False False - 0 - gtk-quit True True True + gtk-quit True + 0 @@ -60,24 +65,24 @@ False False - 0 True True - never - automatic - in + GTK_POLICY_NEVER + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN True - queue + GTK_RESIZE_QUEUE True - vertical + GTK_ORIENTATION_VERTICAL + GTK_ORIENTATION_VERTICAL True @@ -90,7 +95,6 @@ 2 0 0 - 0 @@ -106,7 +110,6 @@ False - 0 @@ -121,7 +124,6 @@ 2 0 0 - 0 @@ -153,8 +155,9 @@ True - vertical + GTK_ORIENTATION_VERTICAL 10 + GTK_ORIENTATION_VERTICAL @@ -169,26 +172,27 @@ True 5 - start + GTK_BUTTONBOX_START - gtk-media-record True True + gtk-media-record True + 0 False False - 0 - gtk-media-stop True True + gtk-media-stop True + 0 False diff --git a/setup.py b/setup.py index e33873c..49bae4c 100755 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ class TestCommand(Command): sources) coverage.report(sources) coverage.erase() - + def _listsources(self, arg, dirname, fnames): fnames = filter(lambda x:x.endswith('.py'), fnames) for name in fnames: @@ -83,7 +83,7 @@ class TestCommand(Command): setup(name='Tutorius', version='0.0', description='Interactive tutor and Tutorial creator', - maintainer='Simon Poirier', + maintainer='Simon Poirier', maintainer_email='simpoir@gmail.com', author='Tutorius team', author_email='sugar-narratives@googlegroups.com', @@ -97,13 +97,15 @@ setup(name='Tutorius', 'sugar.tutorius.apilib.httplib2', ], package_dir={ - 'sugar.tutorius': 'tutorius', - 'sugar.tutorius.addons': 'addons', - }, + 'sugar.tutorius': 'tutorius', + 'sugar.tutorius.addons': 'addons', + }, cmdclass = {'test': TestCommand}, data_files=[('share/icons/sugar/scalable/actions', glob.glob('data/icons/*.svg')), + ('share/icons/sugar/scalable/device', ['data/icons/tutortool.svg']), ('share/tutorius/ui', glob.glob('data/ui/*.glade')), + ('share/sugar/extensions/deviceicon', glob.glob('src/extensions/*')), ] ) -# vim: set et sw=4 sts=4 ts=4: +# vim: set et sw=4 sts=4 ts=4: diff --git a/src/extensions/tutoriusremote.py b/src/extensions/tutoriusremote.py new file mode 100755 index 0000000..d795141 --- /dev/null +++ b/src/extensions/tutoriusremote.py @@ -0,0 +1,134 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Simon Poirier +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" +This modules regroups the UI elements that drives the tutorial and tutorial +creator from the Sugar frame. +""" + +import gtk + +from gettext import gettext as _ +import gconf +import dbus + +import logging + +from sugar.graphics.tray import TrayIcon +from sugar.graphics.palette import Palette +from sugar.graphics.xocolor import XoColor +from sugar.graphics.combobox import ComboBox + +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model.shell import get_model + +from sugar.tutorius.creator import default_creator + +from sugar.tutorius.vault import Vault + +_ICON_NAME = 'tutortool' + +LOGGER = logging.getLogger('remote') + +class TutoriusRemote(TrayIcon): + def __init__(self): + client = gconf.client_get_default() + self._color = XoColor(client.get_string('/desktop/sugar/user/color')) + + super(TutoriusRemote, self).__init__(icon_name=_ICON_NAME, + xo_color=self._color) + + self.set_palette_invoker(FrameWidgetInvoker(self)) + + self.palette = TPalette(_('Tutorius')) + self.palette.set_group_id('frame') + + +class TPalette(Palette): + def __init__(self, primary_text): + super(TPalette, self).__init__(primary_text) + + self._creator_item = gtk.MenuItem(_('Create a tutorial')) + self._creator_item.connect('activate', self._toggle_creator) + self._creator_item.show() + + self._tut_list_item = gtk.MenuItem(_('Show tutorials')) + self._tut_list_item.connect('activate', self._list_tutorials) + self._tut_list_item.show() + + self.menu.append(self._creator_item) + self.menu.append(self._tut_list_item) + + self.set_content(None) + + def _toggle_creator(self, widget): + creator = default_creator() + + if creator.is_authoring == False: + # Replace the start creator label by stop creator + self._creator_item.props.label = _("Stop authoring") + creator.start_authoring(tutorial=None) + + else: + # Attempt to close the creator - this will popup a confirmation + # dialog if the user has unsaved changes + creator.cleanup_cb() + + # If the creator was not actually closed - (in case cancel + # is implemented one day) + if creator.is_authoring == True: + return + # Switch back to start creator entry + self._creator_item.props.label = _("Create a tutorial") + + def _list_tutorials(self, widget): + dlg = gtk.Dialog('Run a tutorial', + None, + gtk.DIALOG_MODAL, + (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)) + dlg.vbox.pack_start(gtk.Label(_('Which tutorial do you want to run?\n'))) + + activity = get_model().get_active_activity() + + act_name = activity.get_activity_name() + tutorial_dict = Vault.list_available_tutorials(act_name) + + # Build the combo box + combo = ComboBox() + for (tuto_name, tuto_guid) in tutorial_dict.items(): + combo.append_item(tuto_name, tuto_guid) + dlg.vbox.pack_end(combo) + dlg.show_all() + + result = dlg.run() + dlg.destroy() + + if result == gtk.RESPONSE_ACCEPT: + row = combo.get_active_item() + if row: + guid = row[0] + name = row[1] + LOGGER.debug("TPalette :: Got message to launch tutorial %s with guid %s"%(str(name), str(guid))) + + from sugar.tutorius.service import ServiceProxy + service = ServiceProxy() + + service.launch(guid) + +def setup(tray): + tray.add_device(TutoriusRemote()) diff --git a/tests/probetests.py b/tests/probetests.py index 92d34a6..357d223 100644 --- a/tests/probetests.py +++ b/tests/probetests.py @@ -96,11 +96,13 @@ class MockProbeProxy(object): def isAlive(self): return self.MockAlive - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): self.MockAction = action self.MockAddressCallback_install = action_installed_cb self.MockInstallErrorCallback = error_cb self.MockActionUpdate = None + self.MockIsEditing = is_editing + self.MockEditCb = editing_cb return None def update(self, action_address, newaction, block=False): @@ -108,9 +110,10 @@ class MockProbeProxy(object): self.MockActionUpdate = newaction return None - def uninstall(self, action_address): + def uninstall(self, action_address, is_editing=False): self.MockAction = None self.MockActionUpdate = None + self.MockIsEditing = None return None def subscribe(self, event, notif_cb, subscribe_cb, error_cb): @@ -224,35 +227,35 @@ class ProbeTest(unittest.TestCase): assert message_box is None, "Message box should still be empty" #install 1 - address = self.probe.install(pickle.dumps(action)) + address = self.probe.install(pickle.dumps(action), False) assert type(address) == str, "install should return a string" assert message_box == (5, "woot"), "message box should have (i, s)" #install 2 action.i, action.s = (10, "ahhah!") - address2 = self.probe.install(pickle.dumps(action)) + address2 = self.probe.install(pickle.dumps(action), False) assert message_box == (10, "ahhah!"), "message box should have changed" assert address != address2, "action addresses should be different" #uninstall 2 - self.probe.uninstall(address2) + self.probe.uninstall(address2, False) assert message_box is None, "undo should clear the message box" #update action 1 with action 2 props - self.probe.update(address, pickle.dumps(action._props)) + self.probe.update(address, pickle.dumps(action._props), False) assert message_box == (10, "ahhah!"), "message box should have changed(i, s)" #ErrorCase: Update with bad address #try to update 2, should fail - self.assertRaises(KeyError, self.probe.update, address2, pickle.dumps(action._props)) + self.assertRaises(KeyError, self.probe.update, address2, pickle.dumps(action._props), False) - self.probe.uninstall(address) + self.probe.uninstall(address, False) assert message_box is None, "undo should clear the message box" message_box = "Test" #ErrorCase: Uninstall bad address (currently silent fail) #Uninstall twice should do nothing - self.probe.uninstall(address) + self.probe.uninstall(address, False) assert message_box == "Test", "undo should not have happened again" def test_events(self): @@ -459,10 +462,10 @@ class ProbeProxyTest(unittest.TestCase): #ErrorCase: Uninstall on not installed action (silent fail) #Test the uninstall - self.probeProxy.uninstall(action2_address) + self.probeProxy.uninstall(action2_address, False) assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed" - self.probeProxy.uninstall(address) + self.probeProxy.uninstall(address, False) assert self.mockObj.MockCall["uninstall"]["args"][0] == address, "1 argument, the action address" def test_events(self): diff --git a/tests/propertiestests.py b/tests/propertiestests.py index cb8e884..49d2312 100644 --- a/tests/propertiestests.py +++ b/tests/propertiestests.py @@ -29,28 +29,28 @@ def try_wrong_values(obj): try: obj.prop = 3 assert False, "Able to insert int value in property of type %s"%typ - except: + except ValueError: pass if typ != "float": try: obj.prop = 1.1 assert False, "Able to insert float value in property of type %s"%typ - except: + except ValueError: pass if typ != "string": try: obj.prop = "Fake string" assert False, "Able to insert string value in property of type %s"%typ - except: + except ValueError: pass if typ != "array": try: obj.prop = [1, 2000, 3, 400] assert False, "Able to insert array value in property of type %s"%typ - except: + except ValueError: pass if typ != "color": @@ -58,7 +58,7 @@ def try_wrong_values(obj): obj.prop = [1,2,3] if typ != "array": assert False, "Able to insert color value in property of type %s"%typ - except: + except ValueError: pass if typ != "boolean": @@ -66,7 +66,7 @@ def try_wrong_values(obj): obj.prop = True if typ != "boolean": assert False, "Able to set boolean value in property of type %s"%typ - except: + except ValueError: pass class BasePropertyTest(unittest.TestCase): diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index b384e6c..eea0465 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -20,14 +20,17 @@ import os import gobject -import dbus import dbus.service import cPickle as pickle +from functools import partial + +from jarabe.model.shell import get_model +from sugar.bundle.activitybundle import ActivityBundle from . import addon +from . import properties from .services import ObjectStore -from .properties import TPropContainer from .dbustools import save_args, ignore, logError import copy @@ -129,11 +132,12 @@ class TProbe(dbus.service.Object): # ------------------ Action handling -------------------------------------- @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='s') - def install(self, pickled_action): + in_signature='sb', out_signature='s') + def install(self, pickled_action, is_editing): """ Install an action on the Activity @param pickled_action string pickled action + @param is_editing whether this action comes from the editor @return string address of installed action """ loaded_action = pickle.loads(str(pickled_action)) @@ -146,17 +150,22 @@ class TProbe(dbus.service.Object): if action._props: action._props.update(loaded_action._props) - action.do(activity=self._activity) - + if not is_editing: + action.do(activity=self._activity) + else: + action.enter_editmode() + action.set_notification_cb(partial(self.update_action, address)) + return address @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='ss', out_signature='') - def update(self, address, action_props): + in_signature='ssb', out_signature='') + def update(self, address, action_props, is_editing): """ Update an already registered action @param address string address returned by install() @param action_props pickled action properties + @param is_editing whether this action comes from the editor @return None """ action = self._installedActions[address] @@ -164,26 +173,47 @@ class TProbe(dbus.service.Object): if action._props: props = pickle.loads(str(action_props)) action._props.update(props) - action.undo() - action.do() + if not is_editing: + action.undo() + action.do() + else: + action.exit_editmode() + action.enter_editmode() @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='') - def uninstall(self, address): + in_signature='sb', out_signature='') + def uninstall(self, address, is_editing): """ Uninstall an action @param address string address returned by install() + @param is_editing whether this action comes from the editor @return None """ if self._installedActions.has_key(address): action = self._installedActions[address] - action.undo() + if not is_editing: + action.undo() + else: + action.exit_editmode() self._installedActions.pop(address) # ------------------ Event handling --------------------------------------- @dbus.service.method("org.tutorius.ProbeInterface", in_signature='s', out_signature='s') + def create_event(self, addon_name): + # avoid recursive imports + event = addon.create(addon_name) + addonname = type(event).__name__ + meta = addon.get_addon_meta(addonname) + for propname in meta['mandatory_props']: + prop = getattr(type(event), propname) + prop.widget_class.run_dialog(self._activity, event, propname) + + return pickle.dumps(event) + + @dbus.service.method("org.tutorius.ProbeInterface", + in_signature='s', out_signature='s') def subscribe(self, pickled_event): """ Subscribe to an Event @@ -216,7 +246,6 @@ class TProbe(dbus.service.Object): @param address string adress returned by subscribe() @return None """ - if self._subscribedEvents.has_key(address): eventfilter = self._subscribedEvents[address] eventfilter.remove_handlers() @@ -237,6 +266,21 @@ class TProbe(dbus.service.Object): else: raise RuntimeWarning("Attempted to raise an unregistered event") + @dbus.service.signal("org.tutorius.ProbeInterface") + def addonUpdated(self, addon_address, pickled_diff_dict): + # Don't do any added processing, the signal will be sent + # when the method exits + pass + + def update_action(self, addon_address, diff_dict): + LOGGER.debug("TProbe :: Trying to update action %s with new property dict %s"%(addon_address, str(diff_dict))) + # Check that this action is installed + if addon_address in self._installedActions.keys(): + LOGGER.debug("TProbe :: Updating action %s"%(addon_address)) + self.addonUpdated(addon_address, pickle.dumps(diff_dict)) + else: + raise RuntimeWarning("Attempted to updated an action that wasn't installed") + # Return a unique name for this action def _generate_action_reference(self, action): # TODO elavoie 2009-07-25 Should return a universal address @@ -284,6 +328,7 @@ class ProbeProxy: self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface") self._actions = {} + self._edition_callbacks = {} # 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 @@ -292,6 +337,17 @@ class ProbeProxy: self._registeredCallbacks = {} self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface") + self._object.connect_to_signal("addonUpdated", self._handle_update_signal, dbus_interface="org.tutorius.ProbeInterface") + + def _handle_update_signal(self, addon_address, pickled_diff_dict): + address = str(addon_address) + diff_dict = pickle.loads(str(pickled_diff_dict)) + LOGGER.debug("ProbeProxy :: Received update property for action %s"%(address)) + # Launch the callback to warn the upper layers of a modification of the addon + # from a widget inside the activity + if self._edition_callbacks.has_key(address): + LOGGER.debug("ProbeProxy :: Executing update callback...") + self._edition_callbacks[address](address, diff_dict) def _handle_signal(self, pickled_event): event = pickle.loads(str(pickled_event)) @@ -312,33 +368,47 @@ class ProbeProxy: except: return False - def __update_action(self, action, callback, address): + def __update_action(self, action, callback, editing_cb, address): LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address)) + address = str(address) + # Store the action self._actions[address] = action + # Store the edition callback + if editing_cb: + self._edition_callbacks[address] = editing_cb + # Propagate the action installed callback upwards in the stack callback(address) def __clear_action(self, address): + # Remove the action installed at this address self._actions.pop(address, None) + # Remove the edition callback + self._edition_callbacks.pop(address, None) - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): """ Install an action on the TProbe's activity @param action Action to install @param action_installed_cb The callback function to call once the action is installed @param error_cb The callback function to call when an error happens + @param is_editing whether this action comes from the editor + @param editing_cb The function to execute when the action is updated + (this is only done in edition mode) @return None """ - self._probe.install(pickle.dumps(action), - reply_handler=save_args(self.__update_action, action, action_installed_cb), - error_handler=save_args(error_cb, action)) + self._probe.install(pickle.dumps(action), + is_editing, + reply_handler=save_args(self.__update_action, action, action_installed_cb, editing_cb), + error_handler=save_args(error_cb, action)) - def update(self, action_address, newaction): + def update(self, action_address, newaction, is_editing=False): """ Update an already installed action's properties and run it again @param action_address The address of the action to update. This is provided by the install callback method. @param newaction Action to update it with @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor @return None """ #TODO review how to make this work well @@ -346,19 +416,20 @@ class ProbeProxy: raise RuntimeWarning("Action not installed") #TODO Check error handling return self._probe.update(action_address, pickle.dumps(newaction._props), + is_editing, reply_handler=ignore, error_handler=logError) - def uninstall(self, action_address): + def uninstall(self, action_address, is_editing): """ Uninstall an installed action @param action_address The address of the action to uninstall. This address was given on action installation - @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor """ if action_address in self._actions: self._actions.pop(action_address, None) - self._probe.uninstall(action_address, reply_handler=ignore, error_handler=logError) + self._probe.uninstall(action_address, is_editing, reply_handler=ignore, error_handler=logError) def __update_event(self, event, callback, event_subscribed_cb, address): LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) @@ -409,7 +480,18 @@ class ProbeProxy: else: LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address) + def create_event(self, addon_name): + """ + Create an event on the app side and request the user to fill the + properties before returning it. + + @param addon_name: the add-on name of the event + @returns: an eventfilter instance + """ + return pickle.loads(str(self._probe.create_event(addon_name))) + def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): + """ Register an event listener @param event Event to listen for @@ -429,11 +511,10 @@ class ProbeProxy: reply_handler=save_args(self.__update_event, event, notification_cb, event_subscribed_cb), error_handler=save_args(error_cb, event)) - def unsubscribe(self, address, block=True): + def unsubscribe(self, address): """ 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)) @@ -445,18 +526,19 @@ class ProbeProxy: else: LOGGER.debug("ProbeProxy :: unsubscribe address %s failed : not registered", address) - def detach(self, block=False): + def detach(self): """ Detach the ProbeProxy from it's TProbe. All installed actions and subscribed events should be removed. """ for action_addr in self._actions.keys(): - self.uninstall(action_addr) + # TODO : Make sure there is a way for each action to be properly + # uninstalled according to its right edition mode + self.uninstall(action_addr, True) for address in self._subscribedEvents.keys(): self.unsubscribe(address) - class ProbeManager(object): """ The ProbeManager provides multiplexing across multiple activity ProbeProxies @@ -482,49 +564,78 @@ class ProbeManager(object): def setCurrentActivity(self, activity_id): if not activity_id in self._probes: - raise RuntimeError("Activity not attached") + raise RuntimeError("Activity not attached, id : %s"%activity_id) self._current_activity = activity_id def getCurrentActivity(self): + # TODO : Insert the correct call to remember the current activity, + # taking the views and frame into account + current_act = get_model().get_active_activity() + current_act_bundle = ActivityBundle(current_act.get_bundle_path()) + current_act_id = current_act_bundle.get_bundle_id() + self._current_activity = current_act_id return self._current_activity currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): """ Install an action on the current activity @param action Action to install @param action_installed_cb The callback to call once the action is installed @param error_cb The callback that will be called if there is an error during installation @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor + @param editing_cb The function to execute when propagating changes on + this action (only used when is_editing is true) @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).install(action, action_installed_cb, error_cb) + return self._first_proxy(self.currentActivity).install( + action=action, + is_editing=is_editing, + action_installed_cb=action_installed_cb, + error_cb=error_cb, + editing_cb=editing_cb) else: raise RuntimeWarning("No activity attached") - def update(self, action_address, newaction): + def update(self, action_address, newaction, is_editing=False): """ Update an already installed action's properties and run it again @param action_address Action to update @param newaction Action to update it with @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).update(action_address, newaction) + return self._first_proxy(self.currentActivity).update(action_address, newaction, is_editing) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action_address): + def uninstall(self, action_address, is_editing=False): """ Uninstall an installed action - @param action Action to uninstall + @param action_address Action to uninstall @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor """ if self.currentActivity: - return self._first_proxy(self.currentActivity).uninstall(action_address) + return self._first_proxy(self.currentActivity).uninstall(action_address, is_editing) + else: + raise RuntimeWarning("No activity attached") + + def create_event(self, addon_name): + """ + Create an event on the app side and request the user to fill the + properties before returning it. + + @param addon_name: the add-on name of the event + @returns: an eventfilter instance + """ + if self.currentActivity: + return self._first_proxy(self.currentActivity).create_event(addon_name) else: raise RuntimeWarning("No activity attached") @@ -601,8 +712,6 @@ class ProbeManager(object): return self._probes[process_name] else: return [] - - def _first_proxy(self, process_name): """ diff --git a/tutorius/actions.py b/tutorius/actions.py index 75c9c9b..8cc5f08 100644 --- a/tutorius/actions.py +++ b/tutorius/actions.py @@ -26,14 +26,21 @@ from . import addon from .services import ObjectStore from .properties import * +import pickle + +import logging + +LOGGER = logging.getLogger("actions") + class DragWrapper(object): """Wrapper to allow gtk widgets to be dragged around""" - def __init__(self, widget, position, draggable=False): + def __init__(self, widget, position, update_action_cb, draggable=False): """ Creates a wrapper to allow gtk widgets to be mouse dragged, if the parent container supports the move() method, like a gtk.Layout. @param widget the widget to enhance with drag capability @param position the widget's position. Will translate the widget if needed + @param update_action_cb The callback to trigger @param draggable wether to enable the drag functionality now """ self._widget = widget @@ -45,6 +52,7 @@ class DragWrapper(object): self.position = position # position of the widget self.moved = False + self.update_action_cb = update_action_cb self.draggable = draggable def _pressed_cb(self, widget, evt): @@ -79,10 +87,13 @@ class DragWrapper(object): self._eventbox.grab_remove() self._dragging = False + LOGGER.debug("DragWrapper :: Sending update notification...") + self.update_action_cb('position', self.position) + def _drag_end(self, *args): """Callback for end of drag (stolen focus).""" self._dragging = False - + def set_draggable(self, value): """Setter for the draggable property""" if bool(value) ^ bool(self._drag_on): @@ -139,6 +150,9 @@ class Action(TPropContainer): TPropContainer.__init__(self) self.position = (0,0) self._drag = None + # The callback that will be triggered when the action is requested + # to notify all its changes + self._properties_updated_cb = None def do(self, **kwargs): """ @@ -152,6 +166,32 @@ class Action(TPropContainer): """ pass #Should raise NotImplemented? + + def set_notification_cb(self, notif_cb): + LOGGER.debug("Action :: Setting notification callback for creator...") + self._properties_updated_cb = notif_cb + + def update_property(self, name, value): + """ + Callback used in the wrapper to send a new value to an action. + """ + LOGGER.debug("Action :: update_property on %s with value '%s'"%(name, str(value))) + # Set the property itself - this will modify the diff dict and we will + # be able to notify the owner with the new value + self.__setattr__(name, value) + + # Send the notification to the creator + self.notify() + + def notify(self): + LOGGER.debug("Action :: Notifying creator with new values in dict : %s"%(str(self._diff_dict))) + # If a notification callback was registered + if self._property_update_cb: + # Propagate it + self._property_update_cb(self._diff_dict) + # Empty the diff dict as we just synchronized with the creator + self._diff_dict.clear() + def enter_editmode(self, **kwargs): """ Enters edit mode. The action should display itself in some way, @@ -171,7 +211,7 @@ class Action(TPropContainer): ObjectStore().activity._overlayer.put(self.__edit_img, x, y) self.__edit_img.show_all() - self._drag = DragWrapper(self.__edit_img, self.position, True) + self._drag = DragWrapper(self.__edit_img, self.position, update_action_cb=self.update_property, draggable=True) def exit_editmode(self, **kwargs): x, y = self._drag.position diff --git a/tutorius/creator.py b/tutorius/creator.py index 68c5fa6..50017dc 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -21,83 +21,145 @@ the activity itself. # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -import gtk.gdk import gtk.glade import gobject from gettext import gettext as T +import pickle + import uuid import os from sugar.graphics import icon, style +import jarabe.frame -from . import overlayer, gtkutils, actions, vault, properties, addon -from . import filters +from . import overlayer, gtkutils, vault, addon from .services import ObjectStore -from .core import State from .tutorial import Tutorial from . import viewer -from .propwidgets import TextInputDialog, StringPropWidget +from .propwidgets import TextInputDialog +from . import TProbe + +from functools import partial + +from dbus import SessionBus +from dbus.service import method, Object, BusName +from .dbustools import ignore + +import logging + +LOGGER = logging.getLogger("creator") + +BUS_PATH = "/org/tutorius/Creator" +BUS_NAME = "org.tutorius.Creator" + +def default_creator(): + """ + The Creator class is a singleton. There can never be more than one creator + at a time. This method returns a new instance only if none + already exists. Else, the existing instance is returned. + """ + return Creator._instance + +def get_creator_proxy(): + """ + Returns a Creator dbus proxy for inter-process events. + """ + bus = SessionBus() + proxy = bus.get_object(BUS_NAME, BUS_PATH) + return proxy -class Creator(object): +class Creator(Object): """ - Class acting as a bridge between the creator, serialization and core - classes. This contains most of the UI part of the editor. + Class acting as a controller for the tutorial edition. """ - def __init__(self, activity, tutorial=None): + + _instance = None + + def __init__(self, probe_manager): + """ + Creates the instance of the creator. It is assumed this will be called + only once, by the Service. + + @param probe_manager The Probe Manager """ - Instanciate a tutorial creator for the activity. + bus_name = BusName(BUS_NAME, bus=SessionBus()) + Object.__init__(self, bus_name, BUS_PATH) - @param activity to bind the creator to - @param tutorial an existing tutorial to edit, or None to create one + self.tuto = None + self.is_authoring = False + if Creator._instance: + raise RuntimeError("Creator was already instanciated") + Creator._instance = self + self._probe_mgr = probe_manager + self._installed_actions = list() + + def start_authoring(self, tutorial=None): + """ + Start authoring a tutorial. + + @type tutorial: str or None + @param tutorial: the unique identifier to an existing tutorial to + modify, or None to create a new one. """ - self._activity = activity + if self.is_authoring: + raise Exception("Already authoring") + + self.is_authoring = True + if not tutorial: self._tutorial = Tutorial('Untitled') self._state = self._tutorial.add_state() self._tutorial.update_transition( transition_name=self._tutorial.INITIAL_TRANSITION_NAME, new_state=self._state) + final_event = addon.create( + name='MessageButtonNext', + message=T('This is the end of this tutorial.') + ) + self._tutorial.add_transition( + state_name=self._state, + transition=(final_event, self._tutorial.END), + ) 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 - 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.metadata = None - self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5)) - self._activity._overlayer.put(self._hlmask, 0, 0) - - dlg_width = 300 - dlg_height = 70 - sw = gtk.gdk.screen_width() - sh = gtk.gdk.screen_height() + frame = jarabe.frame.get_view() - self._propedit = ToolBox(self._activity) + self._propedit = ToolBox(None) self._propedit.tree.signal_autoconnect({ - 'on_quit_clicked': self._cleanup_cb, + '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) - + gtk.gdk.screen_width()-self._propedit.window.get_allocation().width\ + -style.GRID_CELL_SIZE, + style.GRID_CELL_SIZE) + self._propedit.window.connect('enter-notify-event', + frame._enter_notify_cb) + self._propedit.window.connect('leave-notify-event', + frame._leave_notify_cb) self._overview = viewer.Viewer(self._tutorial, self) - self._overview.win.set_transient_for(self._activity) + self._overview.win.set_transient_for(frame._bottom_panel) + self._overview.win.connect('enter-notify-event', + frame._enter_notify_cb) + self._overview.win.connect('leave-notify-event', + frame._leave_notify_cb) - self._overview.win.move(0, gtk.gdk.screen_height()- \ - self._overview.win.get_allocation().height) + self._overview.win.move(style.GRID_CELL_SIZE, + gtk.gdk.screen_height()-style.GRID_CELL_SIZE \ + -self._overview.win.get_allocation().height) self._transitions = dict() @@ -120,7 +182,8 @@ class Creator(object): .get(action, None) if not action_obj: return False - action_obj.exit_editmode() + + self._probe_mgr.uninstall(action_obj.address) self._tutorial.delete_action(action) self._overview.win.queue_draw() return True @@ -133,7 +196,9 @@ class Creator(object): @returns: True if successful, otherwise False. """ - if self._state in (self._tutorial.INIT, self._tutorial.END): + if self._state in (self._tutorial.INIT, self._tutorial.END) \ + or self._tutorial.END in \ + self._tutorial.get_following_states_dict(self._state): # last state cannot be removed return False @@ -144,103 +209,105 @@ class Creator(object): return bool(self._tutorial.delete_state(remove_state)) def get_insertion_point(self): + """ + @returns: the current tutorial insertion point. + """ return self._state def set_insertion_point(self, state_name): - for action in self._tutorial.get_action_dict(self._state).values(): - action.exit_editmode() + """ + Set the tutorial modification point to the specified state. + Actions of the state will enter the edit mode. + New actions will be inserted to that state and new transitions will + shift the current transition to the next state. + + @param state_name: the name of the state to use as insertion point + """ + # first is not modifiable, as the auto transition would make changes + # pointless. The end state is also pointless to modify, as the tutorial + # gets detached. + if state_name == self._tutorial.INIT \ + or state_name == self._tutorial.END: + return + + for action in self._installed_actions: + self._probe_mgr.uninstall(action.address, + is_editing=True) + self._installed_actions = [] self._state = state_name state_actions = self._tutorial.get_action_dict(self._state).values() + for action in state_actions: - action.enter_editmode() - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True, + editing_cb=self.update_addon_property) if state_actions: + # I'm really lazy right now and to keep things simple I simply + # always select the first action when + # we change state. we should really select the clicked block + # in the overview instead. FIXME self._propedit.action = state_actions[0] else: self._propedit.action = None self._overview.win.queue_draw() - - 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. - """ - # undo actions so they don't persist through step editing - for action in self._state.get_action_list(): - action.exit_editmode() - self._hlmask.covered = None - self._propedit.action = None - self._activity.queue_draw() - - def _intro_cb(self, widget, evt): - """ - Callback for capture of widget events, when in introspect mode. - """ - if evt.type == gtk.gdk.BUTTON_PRESS: - # widget has focus, let's hilight it - win = gtk.gdk.display_get_default().get_window_at_pointer() - click_wdg = win[0].get_user_data() - if not click_wdg.is_ancestor(self._activity._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._selected_widget = gtkutils.raddr_lookup(click_wdg) - - if self._eventmenu: - self._eventmenu.destroy() - self._eventmenu = gtk.Menu() - menuitem = gtk.MenuItem(label=type(click_wdg).__name__) - menuitem.set_sensitive(False) - self._eventmenu.append(menuitem) - self._eventmenu.append(gtk.MenuItem()) - - for item in gobject.signal_list_names(click_wdg): - menuitem = gtk.MenuItem(label=item) - menuitem.connect("activate", self._evfilt_cb, item) - self._eventmenu.append(menuitem) - self._eventmenu.show_all() - self._eventmenu.popup(None, None, None, evt.button, evt.time) - self._activity.queue_draw() - def _add_action_cb(self, widget, path): """Callback for the action creation toolbar tool""" action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME] + LOGGER.debug("Creator :: Adding an action = %s"%(action_type)) action = addon.create(action_type) - action.enter_editmode() + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True, + editing_cb=self.update_addon_property) self._tutorial.add_action(self._state, action) - # FIXME: replace following with event catching - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + self._propedit.action = action self._overview.win.queue_draw() def _add_event_cb(self, widget, path): - """Callback for the event creation toolbar tool""" + """ + Callback for the event creation toolbar tool. + + The behaviour of event addition is to push the transition of the current + state to the next (newly created state). + + | + v + .--------. .-------. .--------. + | action |---->| event |---->| action | + '--------' '-------' '--------' + | + .--------. .-----------. v .-------. .--------. + | action |--->| new event |-->| event |---->| action | + '--------' '-----------' '-------' '--------' + The cursor always selects a state (between the action and transition) + The result is what the user expects: inserting before an action will + effectively shift the next transition to the next state. + + """ event_type = self._propedit.events_list[path][ToolBox.ICON_NAME] - 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) - prop.widget_class.run_dialog(self._activity, event, propname) + event = self._probe_mgr.create_event(event_type) event_filters = self._tutorial.get_transition_dict(self._state) # if not at the end of tutorial if event_filters: - old_transition = event_filters.keys()[0] - new_state = self._tutorial.add_state(event_filters[old_transition]) - self._tutorial.update_transition(transition_name=old_transition, - new_state=new_state) + old_name = event_filters.keys()[0] + old_transition = self._tutorial.delete_transition(old_name) + new_state = self._tutorial.add_state( + transition_list=(old_transition,) + ) + self._tutorial.add_transition(state_name=self._state, + transition=(event, new_state), + ) else: # append empty state only if edit inserting at end of linearized @@ -252,22 +319,32 @@ class Creator(object): self.set_insertion_point(new_state) + def properties_changed(self, action): + LOGGER.debug("Creator :: properties_changed for action at address %s to %s"%(action.address)) + address = action.address + self._probe_mgr.update(address, + action, + is_editing=True) + def _action_refresh_cb(self, widget, evt, action): """ Callback for refreshing properties values and notifying the property dialog of the new values. """ - action.exit_editmode() - action.enter_editmode() - self._activity.queue_draw() - # TODO: replace following with event catching - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + # TODO : replace with update + self._probe_mgr.uninstall(action.address, + is_editing=True) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True, + editing_cb=self.update_addon_property) self._propedit.action = action self._overview.win.queue_draw() - def _cleanup_cb(self, *args, **kwargs): + def cleanup_cb(self, *args, **kwargs): """ Quit editing and cleanup interface artifacts. @@ -275,61 +352,117 @@ class Creator(object): """ # undo actions so they don't persist through step editing for action in self._tutorial.get_action_dict(self._state).values(): - action.exit_editmode() + self._probe_mgr.uninstall(action.address, + is_editing=True) - if kwargs.get('force', False): + # TODO : Support quit cancellation - right now,every time we execute this, + # we will forcibly end edition afterwards. It would be nice to keep creating + if not kwargs.get('force', False): + # TODO : Move the dialog in the middle of the screen dialog = gtk.MessageDialog( - parent=self._activity, + parent=self._overview.win, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, - message_format=T('Do you want to save before stopping edition?')) + 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._hlmask.destroy() - self._hlmask = None self._propedit.destroy() self._overview.destroy() - self._activity.queue_draw() - del self._activity._creator + self.is_authoring = False def save(self, widget=None): + """ + Save the currently edited tutorial to bundle, prompting for + a name as needed. + """ if not self._guid: self._guid = str(uuid.uuid1()) dlg = TextInputDialog(parent=self._overview.win, text=T("Enter a tutorial title."), field=T("Title")) - tutorialName = "" - while not tutorialName: tutorialName = dlg.pop() + tutorial_name = "" + while not tutorial_name: + tutorial_name = dlg.pop() dlg.destroy() self._metadata = { vault.INI_GUID_PROPERTY: self._guid, - vault.INI_NAME_PROPERTY: tutorialName, + vault.INI_NAME_PROPERTY: tutorial_name, vault.INI_VERSION_PROPERTY: '1', - 'activities':{os.environ['SUGAR_BUNDLE_NAME']: - os.environ['SUGAR_BUNDLE_VERSION'] - }, } + # FIXME : The environment does not dispose of the appropriate + # variables to inform the creator at this point. We will + # need to iterate inside all the actions and remember + # their sources. + + # FIXME : I insist. This is a hack. + related_activities_dict = {} + related_activities_dict['calculate'] = '27' + + self._metadata['activities'] = dict(related_activities_dict) vault.Vault.saveTutorial(self._tutorial, self._metadata) + def launch(self, *args): + assert False, "REMOVE THIS CALL!!!" + launch = staticmethod(launch) - def launch(*args, **kwargs): + def _action_installed_cb(self, action, address): """ - Launch and attach a creator to the currently running activity. + This is a callback intented to be use to receive actions addresses + after they are installed. + @param address: the address of the newly installed action """ - activity = ObjectStore().activity - if not hasattr(activity, "_creator"): - activity._creator = Creator(activity) - launch = staticmethod(launch) + action.address = address + self._installed_actions.append(action) + + def _dbus_exception(self, event, exception): + """ + This is a callback intented to be use to receive exceptions on remote + DBUS calls. + @param exception: the exception thrown by the remote process + """ + LOGGER.debug("Creator :: Got exception -> %s"%(str(exception))) + + @method(BUS_NAME, + in_signature='', + out_signature='b') + def get_authoring_state(self): + """ + @returns True if the creator is being executed right now, False otherwise. + """ + return self.is_authoring + + def update_addon_property(self, addon_address, diff_dict): + """ + Updates the properties on an addon. + @param addon_address The address of the addon that has the property + @param diff_dict The updates to apply to the property dict. + This is treated as a partial update to the addon's + dictionary and contains at least one property value pair + @returns True if the property was updated, False otherwise + """ + # Look up the registered addresses inside the installed actions + for action in self._installed_actions: + # If this is the correct action + if action.address == addon_address: + # Update its property with the new value + action._props.update(diff_dict) + # Update the property edition dialog with it + self._propedit.action = action + return True + class ToolBox(object): + """ + Palette window for edition tools, including the actions, states and + the editable property list of selected actions. + """ ICON_LABEL = 0 ICON_IMAGE = 1 ICON_NAME = 2 @@ -337,21 +470,26 @@ class ToolBox(object): def __init__(self, parent): super(ToolBox, self).__init__() self.__parent = parent - sugar_prefix = os.getenv("SUGAR_PREFIX",default="/usr") + sugar_prefix = os.getenv("SUGAR_PREFIX", default="/usr") glade_file = os.path.join(sugar_prefix, 'share', 'tutorius', 'ui', 'creator.glade') self.tree = gtk.glade.XML(glade_file) self.window = self.tree.get_widget('mainwindow') + self.window.modify_bg(gtk.STATE_NORMAL, + style.COLOR_TOOLBAR_GREY.get_gdk_color()) self._propbox = self.tree.get_widget('propbox') self._propedits = [] self.window.set_transient_for(parent) + self.window.set_keep_above(True) self._action = None self.actions_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str) - self.actions_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING) + self.actions_list.set_sort_column_id(self.ICON_LABEL, + gtk.SORT_ASCENDING) self.events_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str) - self.events_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING) + self.events_list.set_sort_column_id(self.ICON_LABEL, + gtk.SORT_ASCENDING) for toolname in addon.list_addons(): meta = addon.get_addon_meta(toolname) @@ -361,9 +499,13 @@ class ToolBox(object): label = format_multiline(meta['display_name']) if meta['type'] == addon.TYPE_ACTION: - self.actions_list.append((label, img, toolname, meta['display_name'])) + self.actions_list.append( + (label, img, toolname, meta['display_name']) + ) else: - self.events_list.append((label, img, toolname, meta['display_name'])) + self.events_list.append( + (label, img, toolname, meta['display_name']) + ) iconview_action = self.tree.get_widget('iconview1') iconview_action.set_model(self.actions_list) @@ -413,7 +555,8 @@ class ToolBox(object): #Value field prop = getattr(type(action), propname) - propedit = prop.widget_class(self.__parent, action, propname, self._refresh_action_cb) + propedit = prop.widget_class(self.__parent, action, propname, + self._refresh_action_cb) self._propedits.append(propedit) row.pack_end(propedit.widget) @@ -430,8 +573,7 @@ class ToolBox(object): def _refresh_action_cb(self): if self._action is not None: - self.__parent._creator._action_refresh_cb(None, None, self._action) - + default_creator().properties_changed(self._action) # The purpose of this function is to reformat text, as current IconView # implentation does not insert carriage returns on long lines. diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py index 8c653ac..0b78c53 100644 --- a/tutorius/overlayer.py +++ b/tutorius/overlayer.py @@ -150,8 +150,8 @@ class Overlayer(gtk.Layout): # Since widget is laid out in a Layout box, the Layout will honor the # requested size. Using size_allocate could make a nasty nested loop in # some cases. - self._overlayed.set_size_request(allocation.width, allocation.height) - + if self._overlayed: + self._overlayed.set_size_request(allocation.width, allocation.height) class FrameOverlayer(gtk.Window): def __init__(self): diff --git a/tutorius/properties.py b/tutorius/properties.py index a462782..f273569 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -19,6 +19,7 @@ TutoriusProperties have the same behaviour as python properties (assuming you also use the TPropContainer), with the added benefit of having builtin dialog prompts and constraint validation. """ +import uuid from copy import copy, deepcopy from .constraints import Constraint, \ @@ -35,6 +36,9 @@ from .propwidgets import PropWidget, \ FloatPropWidget, \ IntArrayPropWidget +import logging +LOGGER = logging.getLogger("properties") + class TPropContainer(object): """ A class containing properties. This does the attribute wrapping between @@ -60,6 +64,14 @@ class TPropContainer(object): self._props[attr_name] = propinstance.validate( copy(propinstance.default)) + self.__id = hash(uuid.uuid4()) + # The differences dictionary. This is a structure that holds all the + # modifications that were made to the properties since the action + # was last installed or the last moment the notification was executed. + # Every property change will be logged inside it and it will be sent + # to the creator to update its action edition dialog. + self._diff_dict = {} + def __getattribute__(self, name): """ Process the 'fake' read of properties in the appropriate instance @@ -93,8 +105,11 @@ class TPropContainer(object): try: # We attempt to get the property object with __getattribute__ # to work through inheritance and benefit of the MRO. - return props.__setitem__(name, + real_value = props.__setitem__(name, object.__getattribute__(self, name).validate(value)) + LOGGER.debug("Action :: caching %s = %s in diff dict"%(name, str(value))) + self._diff_dict[name] = value + return real_value except AttributeError: return object.__setattr__(self, name, value) @@ -125,24 +140,8 @@ class TPropContainer(object): """ return deepcopy(self._props) - # 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 isinstance(e2, type(self)) and 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) + return (isinstance(e2, type(self)) and self._props == e2._props) class TutoriusProperty(object): """ diff --git a/tutorius/service.py b/tutorius/service.py index 11a94a5..1564339 100644 --- a/tutorius/service.py +++ b/tutorius/service.py @@ -3,6 +3,7 @@ import dbus from .engine import Engine from .dbustools import remote_call from .TProbe import ProbeManager +from .creator import Creator import logging LOGGER = logging.getLogger("sugar.tutorius.service") @@ -24,6 +25,8 @@ class Service(dbus.service.Object): self._probeMgr = ProbeManager() + Creator(self._probeMgr) + def start(self): """ Start the service itself """ diff --git a/tutorius/store.py b/tutorius/store.py index 565295d..69e74af 100644 --- a/tutorius/store.py +++ b/tutorius/store.py @@ -220,7 +220,7 @@ class StoreProxy(object): installnode = xml.getElementsByTagName("install")[0] installurl = installnode.firstChild.nodeValue - fp = urllib.urlopen(installurl) + fp = urllib2.urlopen(installurl) return fp diff --git a/tutorius/translator.py b/tutorius/translator.py index 4f29078..bd24f8f 100644 --- a/tutorius/translator.py +++ b/tutorius/translator.py @@ -177,7 +177,7 @@ class ResourceTranslator(object): install_error_cb(old_action, exception) # Decorated functions - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): # Make a new copy of the action that we want to install, # because translate() changes the action and we # don't want to modify the caller's action representation @@ -187,7 +187,9 @@ class ResourceTranslator(object): # Send the new action to the probe manager self._probe_manager.install(new_action, save_args(self.action_installed, action_installed_cb), - save_args(self.action_install_error, error_cb, new_action)) + save_args(self.action_install_error, error_cb, new_action), + is_editing=is_editing, + editing_cb=editing_cb) def update(self, action_address, newaction): translated_new_action = copy_module.deepcopy(newaction) diff --git a/tutorius/viewer.py b/tutorius/viewer.py index 56428e1..8041162 100644 --- a/tutorius/viewer.py +++ b/tutorius/viewer.py @@ -65,7 +65,8 @@ class Viewer(object): self.drag_pos = None self.selection = set() - self.win = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.win = gtk.Window(gtk.WINDOW_POPUP) + self.win.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) self.win.set_size_request(400, 200) self.win.set_gravity(gtk.gdk.GRAVITY_SOUTH_WEST) self.win.show() -- cgit v0.9.1