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

<-^^->

Cookbook GUI NG

A new generation of CookBook, this time complete and with GtkBuilder support.
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   
 14     def __str__( self):
 15       return "Recipe: %s (%d ingredients)" % (self.title, len( self.ingredients))
 16   
 17     @classmethod
 18     def new_id(cls):
 19       cls.last_id += 1
 20       return cls.last_id
 21   
 22     def set_id(self, new_id):
 23       if Recipe.last_id < new_id:
 24         Recipe.last_id = new_id
 25       self.id = new_id
 26   
 27     def read_dom_element( self, e):
 28       if e.hasAttribute("id"):
 29         self.set_id(int(e.getAttribute("id")))
 30       titles = e.getElementsByTagName( "title")
 31       if not titles:
 32         raise ValueError( "Could not find title in the dom element")
 33       elif len( titles) > 1:
 34         print >> sys.stderr, "Warning: more than one title in dom element\n", titles
 35       self.title = get_all_text_from_element( titles[0])
 36       texts = e.getElementsByTagName( "text")
 37       if texts:
 38         self.text = get_all_text_from_element( texts[0])
 39       for ie in e.getElementsByTagName( Ingredient.dom_element_name):
 40         i = Ingredient()
 41         i.read_dom_element( ie)
 42         self.add_ingredient( i)
 43   
 44     def get_dom_element( self, doc):
 45       el = doc.createElement( self.dom_element_name)
 46       for attr_name in ['title','text']:
 47         attr_el = doc.createElement( attr_name)
 48         attr_el.appendChild( doc.createTextNode( getattr( self, attr_name)))
 49         el.appendChild( attr_el)
 50       for i in self.ingredients:
 51         ie = i.get_dom_element( doc)
 52         el.appendChild( ie)
 53       return el
 54   
 55     def add_ingredient( self, i):
 56       self.ingredients.append( i)
 57   
 58     def remove_all_ingredients(self):
 59       self.ingredients = []
 60   
 61   
 62   class Ingredient( object):
 63   
 64     dom_element_name = "ingredient"
 65   
 66     def __init__( self, name="", amount=""):
 67       self.name = name
 68       self.amount = amount
 69   
 70     def __str__( self):
 71       if self.amount:
 72         return "%s %s" % (self.amount, self.name)
 73       else:
 74         return "%s" % self.name
 75   
 76     def read_dom_element( self, e):
 77       if e.hasAttribute( "amount"):
 78         self.amount = e.getAttribute( "amount")
 79       self.name = get_all_text_from_element( e)
 80   
 81     def get_dom_element( self, doc):
 82       el = doc.createElement( self.dom_element_name)
 83       if self.amount:
 84         el.setAttribute( "amount", str( self.amount))
 85       el.appendChild( doc.createTextNode( self.name))
 86       return el
 87   
 88   
 89   
 90   class CookBook( object):
 91   
 92     def __init__( self):
 93       self.filename = None
 94       self.recipes = []
 95   
 96     def __str__( self):
 97       return "Cookbook: %d recipes from %s" % (len( self.recipes), self.filename)
 98   
 99     def read_xml_file( self, filename):
100       doc = dom.parse( filename)
101       for el in doc.getElementsByTagName( Recipe.dom_element_name):
102         rec = Recipe()
103         rec.read_dom_element( el)
104         self.recipes.append( rec)
105       self.filename = filename
106   
107     def write_xml_file( self, filename=None):
108       if not filename:
109         filename = self.filename
110       doc = dom.Document()
111       root = doc.createElement( "cookbook")
112       doc.appendChild( root)
113       for rec in self.recipes:
114         e = rec.get_dom_element( doc)
115         root.appendChild( e)
116       f = file( filename, "w")
117       f.write( doc.toxml())
118   
119     def add_recipe( self, recipe):
120       self.recipes.append( recipe)
121   
122     def get_recipe_by_id(self, id):
123       """naive, but works"""
124       for recipe in self.recipes:
125         if recipe.id == id:
126           return recipe
127   
128     def remove_recipe(self, recipe):
129       if recipe in self.recipes:
130         self.recipes.remove(recipe)
131   
132   # help functions
133   
134   def get_all_text_from_element( el):
135       text = ""
136       for ch in el.childNodes:
137           if isinstance( ch, dom.Element):
138               text += get_all_text_from_element( ch)
139           if isinstance( ch, dom.Text):
140               text += ch.data
141       return text
142       
143   # end of help functions
144   
145   
146   if __name__ == "__main__":
147     c = CookBook()
148     c.read_xml_file( "infiles/cookbook2.xml")
149     print c
150     for rec in c.recipes:
151       print rec
152     new = Recipe( "Potato dumplings")
153     new.text = "Preparation instructions for czech potato dumplings"
154     c.add_recipe( new)
155     new.add_ingredient( Ingredient( name="potatoes", amount="500g"))
156     c.write_xml_file( "cookbook2-2.xml")
157     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_lib 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_dialog.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.ok_button = self.builder.get_object("ok_button")
 30       self.title_entry.emit("changed")
 31       if title:
 32         self.dialog.set_title(title)
 33       
 34     def run(self):
 35       result = self.dialog.run()
 36       return result
 37   
 38     def destroy(self):
 39       self.dialog.destroy()
 40   
 41     # -- signal handlers
 42   
 43     def on_add_button_clicked(self, w):
 44       iter = self.ingredient_store.append(["",""])
 45       self.ingredient_treeview.get_selection().select_iter(iter)
 46       
 47     def on_remove_button_clicked(self, w):
 48       model, paths = self.ingredient_treeview.get_selection().get_selected_rows()
 49       for path in paths:
 50         model.remove(model.get_iter(path))
 51   
 52     def on_title_entry_changed(self, entry):
 53       # do not allow OK unless some title is given
 54       text = entry.get_text()
 55       if text.strip():
 56         self.ok_button.set_sensitive(True)
 57       else:
 58         self.ok_button.set_sensitive(False)
 59   
 60     def amount_edited(self, view, row, text):
 61       self.ingredient_store[row][0] = text
 62   
 63     def name_edited(self, view, row, text):
 64       self.ingredient_store[row][1] = text
 65       
 66     # -- public methods
 67   
 68     def get_recipe(self, recipe=None):
 69       """if recipe is given, it puts the data into the recipe,
 70       otherwise it constructs a new one"""
 71       if not recipe:
 72         recipe = Recipe()
 73       recipe.title = self.title_entry.get_text().strip()
 74       start, end = self.text_textbuffer.get_bounds()
 75       recipe.text = self.text_textbuffer.get_text(start, end).strip()
 76       recipe.remove_all_ingredients()
 77       for row in self.ingredient_store:
 78         ing = Ingredient()
 79         ing.amount = row[0]
 80         ing.name = row[1]
 81         recipe.add_ingredient(ing)
 82       return recipe
 83   
 84     def fill_data_from_recipe(self, recipe):
 85       """takes a recipe and populates the dialog with data from it"""
 86       self.title_entry.set_text(recipe.title)
 87       self.text_textbuffer.set_text(recipe.text)
 88       for ing in recipe.ingredients:
 89         self.ingredient_store.append([ing.amount, ing.name])
  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_lib import Recipe, Ingredient, CookBook
 13   from cookbook_ng_recipe_dialog 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_ng.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         ret = d.run()
130         if ret == 1:
131           # put data directly to the recipe
132           recipe = d.get_recipe(recipe)
133           row[1] = recipe.title # update the list store
134           # redisplay the recipe
135           self._show_recipe(recipe)
136           # we assume the recipe was changed, but it is not necessarily true
137           self._log_change()
138         d.destroy()
139   
140     def on_delete_recipe_activate(self, action):
141       path = self._get_selected_recipe_path()
142       if path:
143         row = self.title_store[path]
144         recipe = self.book.get_recipe_by_id(row[0])
145         d = gtk.MessageDialog(self.window,
146                               message_format=_("Remove recipe '%s'?") % recipe.title,
147                               buttons=gtk.BUTTONS_OK_CANCEL,
148                               type=gtk.MESSAGE_QUESTION)
149         answer = d.run()
150         if answer == gtk.RESPONSE_OK:
151           self.book.remove_recipe(recipe)
152           self.title_store.remove(self.title_store.get_iter(path))
153           self._log_change()
154         d.destroy()
155   
156     def on_copy_recipe_activate(self, action):
157       recipe = self._get_selected_recipe()
158       if recipe:
159         d = RecipeDialog(self)
160         d.fill_data_from_recipe(recipe)
161         ret = d.run()
162         if ret == 1:
163           recipe = d.get_recipe()
164           self.book.add_recipe(recipe)
165           self._add_recipe_to_store(recipe)
166           self._log_change()
167         d.destroy()
168   
169     def on_show_about_activate(self, action):
170       b = gtk.Builder()
171       b.add_from_file("infiles/cookbook_ng_about_dialog.ui")
172       d = b.get_object("about_dialog")
173       d.run()
174       d.destroy()
175   
176     def on_advanced_search_activate(self, action):
177       # TODO
178       message = _("Not implemented yet :(")
179       d = gtk.MessageDialog(self.window,
180                             message_format=message,
181                             buttons=gtk.BUTTONS_OK,
182                             type=gtk.MESSAGE_ERROR)
183       d.run()
184       d.destroy()
185   
186     def on_exit_activate(self, action, additional=None):
187       """this is used both for the exit action and the window delete-event
188       signal which is triggered by the window being closed. Because of this,
189       we need to support one additional argument and return True which ensures
190       that the destruction of the window will not continue using the default
191       handler of this event"""
192       if self.book and self._changed:
193         message = _("There are unsaved changes. Really quit?")
194         d = gtk.MessageDialog(self.window,
195                               message_format=message,
196                               buttons=gtk.BUTTONS_OK_CANCEL,
197                               type=gtk.MESSAGE_WARNING)
198         res = d.run()
199         d.destroy()
200         if res != gtk.RESPONSE_OK:
201           return True # do not proceed
202       self.quit_app(None)
203   
204     # other callbacks
205   
206     def on_title_treeview_selection_changed(self, selection):
207       model, paths = selection.get_selected_rows()
208       if len(paths) == 1:
209         recipe_id = model[paths[0]][0]
210         recipe = self.book.get_recipe_by_id(recipe_id)
211         if recipe:
212           self._show_recipe(recipe)
213       self._reset_action_sensitivity()
214   
215     def on_search_entry_changed(self, w):
216       # filter the recipes
217       text = w.get_text()
218       for row in self.title_store:
219         recipe = self.book.get_recipe_by_id(row[0])
220         visible = True
221         for word in text.split():
222           # we search by words to allow different word to match
223           # in a different part of the recipe
224           has_word = False
225           if word in recipe.title or word in recipe.text:
226             has_word = True
227           else:
228             # try ingredients
229             for ing in recipe.ingredients:
230               if word in ing.name or word in ing.amount:
231                 has_word = True
232                 break
233           if not has_word:
234             visible = False
235             break
236         row[2] = visible
237   
238     def quit_app(self, w):
239       gtk.main_quit()
240   
241     # other methods
242   
243     def _add_recipe_to_store(self, recipe):
244       self.title_store.append([recipe.id, recipe.title, True])
245   
246     def _get_selected_recipe_path(self):
247       model, paths = self.title_treeview.get_selection().get_selected_rows()
248       if len(paths) == 1:
249         return paths[0]
250       return None
251   
252     def _get_selected_recipe(self):
253       path = self._get_selected_recipe_path()
254       if path:
255         row = self.title_store[path]
256         recipe = self.book.get_recipe_by_id(row[0])
257         return recipe
258       return None
259   
260     def _show_recipe(self, recipe):
261       view = self.builder.get_object("recipe_view")
262       textbuffer = view.get_buffer()
263       textbuffer.set_text("")
264       # title
265       position = textbuffer.get_end_iter()
266       textbuffer.insert_with_tags_by_name(position, recipe.title, 'larger', 'bold')
267       position = textbuffer.get_end_iter()
268       textbuffer.insert(position, "\n\n")
269       # ingredients
270       for ing in recipe.ingredients:
271         position = textbuffer.get_end_iter()
272         textbuffer.insert_with_tags_by_name(position, str(ing)+"\n", 'italic')
273       position = textbuffer.get_end_iter()
274       textbuffer.insert(position, "\n")
275       # text
276       position = textbuffer.get_end_iter()
277       textbuffer.insert(position, recipe.text)
278   
279     def _set_title(self, title):
280       self.window.set_title("CookBookNG - %s" % title)
281   
282     def _reset_action_sensitivity(self):
283       """this adjusts the sensitivity of avaialable actions accoring to the
284       state of the application data"""
285       actions = ["save_file","new_recipe","edit_recipe","copy_recipe",
286                  "delete_recipe","advanced_search"]
287       if not self.book:
288         # no book has been loaded
289         for aname in actions:
290           action = self.builder.get_object(aname)
291           action.set_sensitive(False)
292       else:
293         # book was loaded
294         path = self._get_selected_recipe_path()
295         if path:
296           # a recipe is selected
297           for aname in actions:
298             action = self.builder.get_object(aname)
299             action.set_sensitive(True)
300         else:
301           # a book is open, but no recipe selected
302           for aname in actions:
303             action = self.builder.get_object(aname)
304             if aname in ["edit_recipe","copy_recipe","delete_recipe"]:
305               action.set_sensitive(False)
306             else:
307               action.set_sensitive(True)
308   
309     def _log_change(self):
310       """is used to monitor change to the content, could be also used
311       to gather information for undo"""
312       self._changed = True
313   
314     def _cleanup(self):
315       self.title_store.clear()
316       self.builder.get_object("recipe_view").get_buffer().set_text("")
317       self._set_title("untitled")
318       self._reset_action_sensitivity()
319       self._changed = False
320       
321     # public methods
322   
323     def load_file(self, fullname):
324       self._cleanup()
325       self.book = CookBook()
326       try:
327         self.book.read_xml_file(fullname)
328       except Exception, e:
329         message = _("""It was not possible to load the file '%s'.
330   The error details are shown below:\n\n""") % fullname
331         message += str(e)
332         d = gtk.MessageDialog(self.window,
333                               message_format=message,
334                               buttons=gtk.BUTTONS_OK,
335                               type=gtk.MESSAGE_ERROR)
336         d.run()
337         d.destroy()
338         return
339       r_count = len(self.book.recipes)
340       self.statusbar.push(-1, ngettext("Loaded %d recipe",
341                                        "Loaded %d recipes",
342                                        r_count) % r_count)
343       for recipe in self.book.recipes:
344         self._add_recipe_to_store(recipe)
345       self._set_title(fullname)
346       self._reset_action_sensitivity()
347       
348   app = CookBookNG()
349   import sys
350   if len(sys.argv) > 1:
351     app.load_file(sys.argv[1])
352   gtk.main()
353   
354   # --------------- TODO -----------------
355   # * advanced search
356   # * tags
357   # * list of already known ingredients in recipe dialog
358   # * allow direct saving of current work when asking about
359   #   usaved changes
Screenshot:
Program screenshot cookbook_ng1_1.pngProgram screenshot cookbook_ng1_1a.png
Doba běhu: 1360.3 ms