Python-Ref > GUI programming with PyGTK > Real world example > Cookbook GUI NG - version 2.0
 
 

<-^^

Cookbook GUI NG - version 2.0

An improved version of new generation of CookBook.
The lines that are new or changed from the previous version version are highlighted.
Expand/Shrink
<?xml version="1.0" ?>
<cookbook><recipe><title>Cheese cake</title><text>Here comes the description for preparation of a cheese cake.</text><ingredient amount="500g">soft cheese</ingredient><ingredient amount="2">eggs</ingredient><ingredient amount="100g">sugar</ingredient><ingredient>raisins</ingredient><tag>vege</tag><tag>sweet</tag></recipe><recipe><title>Rosted duck</title><text>Here we describe how to prepare a roasted duck.</text><ingredient amount="1">duck</ingredient><ingredient>cumin</ingredient><ingredient amount="1">apple</ingredient><tag>meat</tag></recipe><recipe><title>Dalsi pokus</title><text>Zase neco AS;JK;ASLDks
asd
aDSASD
ASDAS;Dk;'D
asdASD</text><ingredient amount="20 g">mrkve</ingredient><ingredient amount="30 ml">mlika</ingredient></recipe><recipe><title>Dalsi pokus II</title><text>Zase neco AS;JK;ASLDks
asd
aDSASD
ASDAS;Dk;'D
asdASD</text><ingredient amount="20 g">mrkve</ingredient><ingredient amount="30 ml">mlika</ingredient></recipe></cookbook>
  1   import xml.dom.minidom as dom
  2   
  3   class Recipe( object):
  4   
  5     dom_element_name = "recipe"
  6     last_id = 0
  7   
  8     def __init__( self, title="", text=""):
  9       self.id = Recipe.new_id()
 10       self.title = title
 11       self.text = text
 12       self.ingredients = []
 13       self.tags = [] 
 14   
 15     def __str__( self):
 16       return "Recipe: %s (%d ingredients)" % (self.title, len( self.ingredients))
 17   
 18     @classmethod
 19     def new_id(cls):
 20       cls.last_id += 1
 21       return cls.last_id
 22   
 23     def set_id(self, new_id):
 24       if Recipe.last_id < new_id:
 25         Recipe.last_id = new_id
 26       self.id = new_id
 27   
 28     def read_dom_element( self, e):
 29       if e.hasAttribute("id"):
 30         self.set_id(int(e.getAttribute("id")))
 31       titles = e.getElementsByTagName( "title")
 32       if not titles:
 33         raise ValueError( "Could not find title in the dom element")
 34       elif len( titles) > 1:
 35         print >> sys.stderr, "Warning: more than one title in dom element\n", titles
 36       self.title = get_all_text_from_element( titles[0])
 37       texts = e.getElementsByTagName( "text")
 38       if texts:
 39         self.text = get_all_text_from_element( texts[0])
 40       for ie in e.getElementsByTagName( Ingredient.dom_element_name):
 41         i = Ingredient()
 42         i.read_dom_element( ie)
 43         self.add_ingredient( i)
 44       for te in e.getElementsByTagName("tag"): 
 45         self.add_tag(get_all_text_from_element(te)) 
 46   
 47     def get_dom_element( self, doc):
 48       el = doc.createElement( self.dom_element_name)
 49       for attr_name in ['title','text']:
 50         attr_el = doc.createElement( attr_name)
 51         attr_el.appendChild( doc.createTextNode( getattr( self, attr_name)))
 52         el.appendChild( attr_el)
 53       for i in self.ingredients:
 54         ie = i.get_dom_element( doc)
 55         el.appendChild( ie)
 56       for tag in self.tags: 
 57         tag_el = doc.createElement("tag") 
 58         tag_el.appendChild(doc.createTextNode( tag)) 
 59         el.appendChild(tag_el) 
 60       return el
 61   
 62     def add_ingredient( self, i):
 63       self.ingredients.append( i)
 64   
 65     def remove_all_ingredients(self):
 66       self.ingredients = []
 67   
 68     def add_tag(self, tag): 
 69       self.tags.append(tag) 
 70    
 71     def remove_all_tags(self): 
 72       self.tags = [] 
 73    
 74   
 75   class Ingredient( object):
 76   
 77     dom_element_name = "ingredient"
 78   
 79     def __init__( self, name="", amount=""):
 80       self.name = name
 81       self.amount = amount
 82   
 83     def __str__( self):
 84       if self.amount:
 85         return "%s %s" % (self.amount, self.name)
 86       else:
 87         return "%s" % self.name
 88   
 89     def read_dom_element( self, e):
 90       if e.hasAttribute( "amount"):
 91         self.amount = e.getAttribute( "amount")
 92       self.name = get_all_text_from_element( e)
 93   
 94     def get_dom_element( self, doc):
 95       el = doc.createElement( self.dom_element_name)
 96       if self.amount:
 97         el.setAttribute( "amount", str( self.amount))
 98       el.appendChild( doc.createTextNode( self.name))
 99       return el
100   
101   
102   
103   class CookBook( object):
104   
105     def __init__( self):
106       self.filename = None
107       self.recipes = []
108   
109     def __str__( self):
110       return "Cookbook: %d recipes from %s" % (len( self.recipes), self.filename)
111   
112     def read_xml_file( self, filename):
113       doc = dom.parse( filename)
114       for el in doc.getElementsByTagName( Recipe.dom_element_name):
115         rec = Recipe()
116         rec.read_dom_element( el)
117         self.recipes.append( rec)
118       self.filename = filename
119   
120     def write_xml_file( self, filename=None):
121       if not filename:
122         filename = self.filename
123       doc = dom.Document()
124       root = doc.createElement( "cookbook")
125       doc.appendChild( root)
126       for rec in self.recipes:
127         e = rec.get_dom_element( doc)
128         root.appendChild( e)
129       f = file( filename, "w")
130       f.write( doc.toxml())
131   
132     def add_recipe( self, recipe):
133       self.recipes.append( recipe)
134   
135     def get_recipe_by_id(self, id):
136       """naive, but works"""
137       for recipe in self.recipes:
138         if recipe.id == id:
139           return recipe
140   
141     def remove_recipe(self, recipe):
142       if recipe in self.recipes:
143         self.recipes.remove(recipe)
144   
145   # help functions
146   
147   def get_all_text_from_element( el):
148       text = ""
149       for ch in el.childNodes:
150           if isinstance( ch, dom.Element):
151               text += get_all_text_from_element( ch)
152           if isinstance( ch, dom.Text):
153               text += ch.data
154       return text
155       
156   # end of help functions
157   
158   
159   if __name__ == "__main__":
160     c = CookBook()
161     c.read_xml_file( "infiles/cookbook2.xml")
162     print c
163     for rec in c.recipes:
164       print rec
165     new = Recipe( "Potato dumplings")
166     new.text = "Preparation instructions for czech potato dumplings"
167     c.add_recipe( new)
168     new.add_ingredient( Ingredient( name="potatoes", amount="500g"))
169     c.write_xml_file( "cookbook2-2.xml")
170     print c
  1   # standard libraries
  2   import os
  3   
  4   # third party libraries
  5   import pygtk
  6   import gtk
  7   pygtk.require("2.0")
  8   
  9   # local imports
 10   from cookbook_ng_lib2 import Recipe, Ingredient 
 11   
 12   
 13   class RecipeDialog(object):
 14   
 15     def __init__(self, parent, title=""):
 16       """
 17       client = dslib client instance
 18       """
 19       self.parent = parent
 20       self.builder = gtk.Builder()
 21       self.builder.add_from_file("infiles/recipe_dialog2.ui") 
 22       self.builder.connect_signals(self)
 23       self.dialog = self.builder.get_object("dialog1")
 24       self.text_textview = self.builder.get_object("text_textview")
 25       self.text_textbuffer = self.text_textview.get_buffer()
 26       self.title_entry = self.builder.get_object("title_entry")
 27       self.ingredient_store = self.builder.get_object("ingredient_store")
 28       self.ingredient_treeview = self.builder.get_object("ingredient_treeview")
 29       self.tag_store = self.builder.get_object("tag_store") 
 30       self.all_tag_store = self.builder.get_object("all_tag_store") 
 31       self.ok_button = self.builder.get_object("ok_button")
 32       self.title_entry.emit("changed")
 33       if title:
 34         self.dialog.set_title(title)
 35       
 36     def run(self):
 37       result = self.dialog.run()
 38       return result
 39   
 40     def destroy(self):
 41       self.dialog.destroy()
 42   
 43     # -- signal handlers
 44   
 45     def on_add_button_clicked(self, w):
 46       iter = self.ingredient_store.append(["",""])
 47       self.ingredient_treeview.get_selection().select_iter(iter)
 48       
 49     def on_remove_button_clicked(self, w):
 50       model, paths = self.ingredient_treeview.get_selection().get_selected_rows()
 51       for path in paths:
 52         model.remove(model.get_iter(path))
 53   
 54     def on_title_entry_changed(self, entry):
 55       # do not allow OK unless some title is given
 56       text = entry.get_text()
 57       if text.strip():
 58         self.ok_button.set_sensitive(True)
 59       else:
 60         self.ok_button.set_sensitive(False)
 61   
 62     def amount_edited(self, view, row, text):
 63       self.ingredient_store[row][0] = text
 64   
 65     def name_edited(self, view, row, text):
 66       self.ingredient_store[row][1] = text
 67       
 68     def on_tagcellrenderer_changed(self, w, row, new_value_iter): 
 69       # in this case new_value_iter is not a text - it is an iter to all_tag_store 
 70       new_value = self.all_tag_store[new_value_iter][0] 
 71       # reuse the on_tagcellrenderer_edited method 
 72       self.on_tagcellrenderer_edited(w, row, new_value) 
 73    
 74     def on_tagcellrenderer_edited(self, w, row, new_value): 
 75       new_value = new_value.strip() 
 76       self.tag_store[row][0] = new_value 
 77       iter = self.tag_store.get_iter(row) 
 78       if new_value: 
 79         if not self.tag_store.iter_next(iter): 
 80           # there is now next row - we add one empty for new data 
 81           self.tag_store.append([""]) 
 82         # remove already used tag from available tags 
 83         iter = self.all_tag_store.get_iter_first() 
 84         while iter: 
 85           if self.all_tag_store.get_value(iter, 0) == new_value: 
 86             self.all_tag_store.remove(iter) 
 87             break 
 88           iter = self.all_tag_store.iter_next(iter) 
 89    
 90     def on_tag_treeview_key_release_event(self, w, event): 
 91       """when delete or something is pressed on a tag column""" 
 92       if event.keyval == gtk.keysyms.Delete: 
 93         selection = self.builder.get_object("tag_treeview").get_selection() 
 94         model, paths = selection.get_selected_rows() 
 95         for path in paths: 
 96           if model[path][0]: 
 97             # do not delete empty rows 
 98             model.remove(model.get_iter(path)) 
 99    
100     # -- public methods
101   
102     def get_recipe(self, recipe=None):
103       """if recipe is given, it puts the data into the recipe,
104       otherwise it constructs a new one"""
105       if not recipe:
106         recipe = Recipe()
107       recipe.title = self.title_entry.get_text().strip()
108       start, end = self.text_textbuffer.get_bounds()
109       recipe.text = self.text_textbuffer.get_text(start, end).strip()
110       recipe.remove_all_ingredients()
111       recipe.remove_all_tags() 
112       for row in self.ingredient_store:
113         ing = Ingredient()
114         ing.amount = row[0]
115         ing.name = row[1]
116         recipe.add_ingredient(ing)
117       for row in self.tag_store: 
118         if row[0].strip(): 
119           recipe.add_tag(row[0]) 
120       return recipe
121   
122     def fill_data_from_recipe(self, recipe):
123       """takes a recipe and populates the dialog with data from it"""
124       self.title_entry.set_text(recipe.title)
125       self.text_textbuffer.set_text(recipe.text)
126       for ing in recipe.ingredients:
127         self.ingredient_store.append([ing.amount, ing.name])
128       for tag in recipe.tags: 
129         self.tag_store.append([tag]) 
130       self.tag_store.append([""]) # empty place for new tag 
131    
132     def fill_available_tags(self, tags): 
133       """fill in all the available tags""" 
134       # this store holds all avaialable tags - not only the ones used in this recipe 
135       for tag in tags: 
136         self.all_tag_store.append([tag]) 
  1   # standard libraries
  2   import gettext
  3   gettext.install("cookbookng", names=("ngettext",))
  4   
  5   # third party libraries
  6   import pygtk
  7   pygtk.require("2.0")
  8   import gtk
  9   import pango
 10   
 11   # local libraries
 12   from cookbook_ng_lib2 import Recipe, Ingredient, CookBook 
 13   from cookbook_ng_recipe_dialog2 import RecipeDialog 
 14   
 15   class CookBookNG(object):
 16   
 17     def __init__(self):
 18       self.book = None  # here will a CookBook instance be
 19       self._changed = False  # this will monitor changes
 20       self.builder = gtk.Builder()
 21       self.builder.add_from_file("infiles/cookbook_ng2.ui")  
 22       self.builder.connect_signals(self)
 23       self.window = self.builder.get_object("window1")
 24       self.window.connect("destroy", self.quit_app)
 25       self.statusbar = self.builder.get_object("statusbar")
 26       self.title_store = self.builder.get_object("title_store")
 27       self.title_treeview = self.builder.get_object("title_treeview")
 28       # TreeModelFilter does not work well with Glade
 29       # create a filter
 30       title_filter = self.title_store.filter_new()
 31       title_filter.set_visible_column(2)
 32       # replace the model with the filter
 33       self.title_treeview.set_model(title_filter)
 34       # manual callback addition to selection
 35       self.title_treeview.get_selection().\
 36                connect("changed",self.on_title_treeview_selection_changed)
 37       # manual creation of text tags
 38       text_buffer = self.builder.get_object("recipe_view").get_buffer()
 39       text_buffer.create_tag("larger", scale=1.25, pixels_above_lines=10)
 40       text_buffer.create_tag("bold", weight=pango.WEIGHT_BOLD)
 41       text_buffer.create_tag("italic", style=pango.STYLE_ITALIC)
 42       # show the main window
 43       self._reset_action_sensitivity()
 44       self.window.show()
 45   
 46     # action callbacks
 47   
 48     def on_new_file_activate(self, action):
 49       if self.book and self._changed:
 50         message = _("There are unsaved changes. Continue?")
 51         d = gtk.MessageDialog(self.window,
 52                               message_format=message,
 53                               buttons=gtk.BUTTONS_OK_CANCEL,
 54                               type=gtk.MESSAGE_WARNING)
 55         res = d.run()
 56         d.destroy()
 57         if res != gtk.RESPONSE_OK:
 58           return # do not proceed
 59       self.book = CookBook()
 60       self._cleanup()
 61   
 62     def on_save_file_activate(self, action):
 63       if not self.book.filename:
 64         d = gtk.FileChooserDialog(title=_("Save cookbook to file"),
 65                                 parent=self.window,
 66                                 action=gtk.FILE_CHOOSER_ACTION_SAVE,
 67                                 buttons=(gtk.STOCK_OK,gtk.RESPONSE_ACCEPT,
 68                                          gtk.STOCK_CANCEL,gtk.RESPONSE_REJECT)
 69                                 )
 70         res = d.run()
 71         if res == gtk.RESPONSE_ACCEPT:
 72           self.book.filename = d.get_filename()
 73           self.book.write_xml_file()
 74           self._changed = False
 75         d.destroy()
 76       else:
 77         self.book.write_xml_file()
 78         self._changed = False
 79       
 80     def on_open_file_activate(self, action):
 81       if self.book and self._changed:
 82         message = _("There are unsaved changes. Continue?")
 83         d = gtk.MessageDialog(self.window,
 84                               message_format=message,
 85                               buttons=gtk.BUTTONS_OK_CANCEL,
 86                               type=gtk.MESSAGE_WARNING)
 87         res = d.run()
 88         d.destroy()
 89         if res != gtk.RESPONSE_OK:
 90           return # do not proceed
 91       d = gtk.FileChooserDialog(title=_("Select cookbook to open"),
 92                                 parent=self.window,
 93                                 action=gtk.FILE_CHOOSER_ACTION_OPEN,
 94                                 buttons=(gtk.STOCK_OK,gtk.RESPONSE_ACCEPT,
 95                                          gtk.STOCK_CANCEL,gtk.RESPONSE_REJECT)
 96                                 )
 97       # create filters
 98       f2 = gtk.FileFilter()
 99       f2.set_name( "Cookbooks (*.cb)")
100       f2.add_pattern( "*.cb")
101       d.add_filter( f2)
102       f1 = gtk.FileFilter()
103       f1.set_name( "All files (*)")
104       f1.add_pattern( "*")
105       d.add_filter( f1)
106       ok = d.run()
107       if ok == gtk.RESPONSE_ACCEPT:
108         fullname = d.get_filename()
109         self.load_file(fullname)      
110       d.destroy()
111   
112     def on_new_recipe_activate(self, action):
113       d = RecipeDialog(self)
114       ret = d.run()
115       if ret == 1:
116         recipe = d.get_recipe()
117         self.book.add_recipe(recipe)
118         self._add_recipe_to_store(recipe)
119         self._log_change()
120       d.destroy()
121   
122     def on_edit_recipe_activate(self, action):
123       path = self._get_selected_recipe_path()
124       if path:
125         row = self.title_store[path]
126         recipe = self.book.get_recipe_by_id(row[0])
127         d = RecipeDialog(self)
128         d.fill_data_from_recipe(recipe)
129         d.fill_available_tags(self.get_all_tags()) 
130         ret = d.run()
131         if ret == 1:
132           # put data directly to the recipe
133           recipe = d.get_recipe(recipe)
134           row[1] = recipe.title # update the list store
135           # redisplay the recipe
136           self._show_recipe(recipe)
137           # we assume the recipe was changed, but it is not necessarily true
138           self._log_change()
139         d.destroy()
140   
141     def on_delete_recipe_activate(self, action):
142       path = self._get_selected_recipe_path()
143       if path:
144         row = self.title_store[path]
145         recipe = self.book.get_recipe_by_id(row[0])
146         d = gtk.MessageDialog(self.window,
147                               message_format=_("Remove recipe '%s'?") % recipe.title,
148                               buttons=gtk.BUTTONS_OK_CANCEL,
149                               type=gtk.MESSAGE_QUESTION)
150         answer = d.run()
151         if answer == gtk.RESPONSE_OK:
152           self.book.remove_recipe(recipe)
153           self.title_store.remove(self.title_store.get_iter(path))
154           self._log_change()
155         d.destroy()
156   
157     def on_copy_recipe_activate(self, action):
158       recipe = self._get_selected_recipe()
159       if recipe:
160         d = RecipeDialog(self)
161         d.fill_data_from_recipe(recipe)
162         ret = d.run()
163         if ret == 1:
164           recipe = d.get_recipe()
165           self.book.add_recipe(recipe)
166           self._add_recipe_to_store(recipe)
167           self._log_change()
168         d.destroy()
169   
170     def on_show_about_activate(self, action):
171       b = gtk.Builder()
172       b.add_from_file("infiles/cookbook_ng_about_dialog.ui")
173       d = b.get_object("about_dialog")
174       d.run()
175       d.destroy()
176   
177     def on_advanced_search_activate(self, action):
178       # TODO
179       message = _("Not implemented yet :(")
180       d = gtk.MessageDialog(self.window,
181                             message_format=message,
182                             buttons=gtk.BUTTONS_OK,
183                             type=gtk.MESSAGE_ERROR)
184       d.run()
185       d.destroy()
186   
187     def on_exit_activate(self, action, additional=None):
188       """this is used both for the exit action and the window delete-event
189       signal which is triggered by the window being closed. Because of this,
190       we need to support one additional argument and return True which ensures
191       that the destruction of the window will not continue using the default
192       handler of this event"""
193       if self.book and self._changed:
194         message = _("There are unsaved changes. Really quit?")
195         d = gtk.MessageDialog(self.window,
196                               message_format=message,
197                               buttons=gtk.BUTTONS_OK_CANCEL,
198                               type=gtk.MESSAGE_WARNING)
199         res = d.run()
200         d.destroy()
201         if res != gtk.RESPONSE_OK:
202           return True # do not proceed
203       self.quit_app(None)
204   
205     # other callbacks
206   
207     def on_title_treeview_selection_changed(self, selection):
208       model, paths = selection.get_selected_rows()
209       if len(paths) == 1:
210         recipe_id = model[paths[0]][0]
211         recipe = self.book.get_recipe_by_id(recipe_id)
212         if recipe:
213           self._show_recipe(recipe)
214       self._reset_action_sensitivity()
215   
216     def on_search_entry_changed(self, w):
217       # filter the recipes
218       text = w.get_text()
219       for row in self.title_store:
220         recipe = self.book.get_recipe_by_id(row[0])
221         visible = True
222         for word in text.split():
223           # we search by words to allow different word to match
224           # in a different part of the recipe
225           has_word = False
226           if word in recipe.title or word in recipe.text:
227             has_word = True
228           else:
229             # try ingredients
230             for ing in recipe.ingredients:
231               if word in ing.name or word in ing.amount:
232                 has_word = True
233                 break
234           if not has_word:
235             # try tags 
236             for tag in recipe.tags: 
237               if word in tag: 
238                 has_word = True 
239                 break 
240           if not has_word: 
241             visible = False
242             break
243         row[2] = visible
244   
245     def quit_app(self, w):
246       gtk.main_quit()
247   
248     # other methods
249   
250     def _add_recipe_to_store(self, recipe):
251       self.title_store.append([recipe.id, recipe.title, True])
252   
253     def _get_selected_recipe_path(self):
254       model, paths = self.title_treeview.get_selection().get_selected_rows()
255       if len(paths) == 1:
256         return paths[0]
257       return None
258   
259     def _get_selected_recipe(self):
260       path = self._get_selected_recipe_path()
261       if path:
262         row = self.title_store[path]
263         recipe = self.book.get_recipe_by_id(row[0])
264         return recipe
265       return None
266   
267     def _show_recipe(self, recipe):
268       view = self.builder.get_object("recipe_view")
269       textbuffer = view.get_buffer()
270       textbuffer.set_text("")
271       # title
272       position = textbuffer.get_end_iter()
273       textbuffer.insert_with_tags_by_name(position, recipe.title, 'larger', 'bold')
274       textbuffer.insert(position, "\n\n")
275       # tags 
276       tag_text = "; ".join(recipe.tags) 
277       textbuffer.insert_with_tags_by_name(position, _("Tags:")+" ", 'bold') 
278       textbuffer.insert(position, tag_text+"\n\n") 
279       # ingredients
280       for ing in recipe.ingredients:
281         textbuffer.insert_with_tags_by_name(position, str(ing)+"\n", 'italic')
282       textbuffer.insert(position, "\n")
283       # text
284       textbuffer.insert(position, recipe.text)
285   
286     def _set_title(self, title):
287       self.window.set_title("CookBookNG - %s" % title)
288   
289     def _reset_action_sensitivity(self):
290       """this adjusts the sensitivity of avaialable actions accoring to the
291       state of the application data"""
292       actions = ["save_file","new_recipe","edit_recipe","copy_recipe",
293                  "delete_recipe","advanced_search"]
294       if not self.book:
295         # no book has been loaded
296         for aname in actions:
297           action = self.builder.get_object(aname)
298           action.set_sensitive(False)
299       else:
300         # book was loaded
301         path = self._get_selected_recipe_path()
302         if path:
303           # a recipe is selected
304           for aname in actions:
305             action = self.builder.get_object(aname)
306             action.set_sensitive(True)
307         else:
308           # a book is open, but no recipe selected
309           for aname in actions:
310             action = self.builder.get_object(aname)
311             if aname in ["edit_recipe","copy_recipe","delete_recipe"]:
312               action.set_sensitive(False)
313             else:
314               action.set_sensitive(True)
315   
316     def _log_change(self):
317       """is used to monitor change to the content, could be also used
318       to gather information for undo"""
319       self._changed = True
320   
321     def _cleanup(self):
322       self.title_store.clear()
323       self.builder.get_object("recipe_view").get_buffer().set_text("")
324       self._set_title("untitled")
325       self._reset_action_sensitivity()
326       self._changed = False
327       
328     # public methods
329   
330     def load_file(self, fullname):
331       self._cleanup()
332       self.book = CookBook()
333       try:
334         self.book.read_xml_file(fullname)
335       except Exception, e:
336         message = _("""It was not possible to load the file '%s'.
337   The error details are shown below:\n\n""") % fullname
338         message += str(e)
339         d = gtk.MessageDialog(self.window,
340                               message_format=message,
341                               buttons=gtk.BUTTONS_OK,
342                               type=gtk.MESSAGE_ERROR)
343         d.run()
344         d.destroy()
345         return
346       r_count = len(self.book.recipes)
347       self.statusbar.push(-1, ngettext("Loaded %d recipe",
348                                        "Loaded %d recipes",
349                                        r_count) % r_count)
350       for recipe in self.book.recipes:
351         self._add_recipe_to_store(recipe)
352       self._set_title(fullname)
353       self._reset_action_sensitivity()
354    
355     def get_all_tags(self): 
356       tags = set() 
357       for recipe in self.book.recipes: 
358         for tag in recipe.tags: 
359           tags.add(tag) 
360       tags = list(tags) 
361       tags.sort() 
362       return tags 
363      
364       
365   app = CookBookNG()
366   import sys
367   if len(sys.argv) > 1:
368     app.load_file(sys.argv[1])
369   gtk.main()
370   
371   # --------------- TODO -----------------
372   # * advanced search
373   # * list of already known ingredients in recipe dialog
374   # * allow direct saving of current work when asking about
375   #   usaved changes
Screenshot:
Program screenshot cookbook_ng2_1.png
Doba běhu: 1247.1 ms