1
"""A fallback interactive importer.
3
This is handed generic text. We then guide the user through the text
4
(they tell us what's an instruction, what's an ingredient, etc.)
7
import gtk, gtk.glade, gtk.gdk, pango
8
import re, os.path, sys
9
from gourmet import convert
10
from gourmet import gglobals
11
from gourmet import check_encodings
2
from gettext import gettext as _
3
from generic_recipe_parser import RecipeParser
4
import gourmet.gtk_extras.cb_extras as cb
5
import gourmet.gglobals as gglobals
13
from generic_recipe_parser import RecipeParser
8
import gourmet.convert as convert
9
from gourmet.threadManager import NotThreadSafe
14
10
import imageBrowser
15
from gettext import gettext as _
19
# Module that provides an object oriented abstraction to pygtk and libglade.
20
# Copyright (C) 2004 Sandino Flores Moreno
28
def __init__(self, path, root=None, domain=None, **kwargs):
30
Load a glade file specified by glade_filename, using root as
31
root widget and domain as the domain for translations.
33
If it receives extra named arguments (argname=value), then they are used
34
as attributes of the instance.
37
path to a glade filename.
38
If glade_filename cannot be found, then it will be searched in the
39
same directory of the program (sys.argv[0])
42
the name of the widget that is the root of the user interface,
43
usually a window or dialog (a top level widget).
44
If None or ommited, the full user interface is loaded.
47
A domain to use for loading translations.
48
If None or ommited, no translation is loaded.
51
a dictionary representing the named extra arguments.
52
It is useful to set attributes of new instances, for example:
53
glade_app = SimpleGladeApp("ui.glade", foo="some value", bar="another value")
54
sets two attributes (foo and bar) to glade_app.
56
if os.path.isfile(path):
57
self.glade_path = path
59
glade_dir = os.path.dirname( sys.argv[0] )
60
self.glade_path = os.path.join(glade_dir, path)
61
for key, value in kwargs.items():
63
setattr(self, key, weakref.proxy(value) )
65
setattr(self, key, value)
67
#self.install_custom_handler(self.custom_handler)
68
self.glade = self.create_glade(self.glade_path, root, domain)
70
self.main_widget = self.get_widget(root)
72
self.main_widget = None
73
self.normalize_names()
74
self.add_callbacks(self)
78
class_name = self.__class__.__name__
80
root = gtk.Widget.get_name(self.main_widget)
81
repr = '%s(path="%s", root="%s")' % (class_name, self.glade_path, root)
83
repr = '%s(path="%s")' % (class_name, self.glade_path)
88
Method called when the user interface is loaded and ready to be used.
89
At this moment, the widgets are loaded and can be refered as self.widget_name
93
def add_callbacks(self, callbacks_proxy):
95
It uses the methods of callbacks_proxy as callbacks.
96
The callbacks are specified by using:
97
Properties window -> Signals tab
98
in glade-2 (or any other gui designer like gazpacho).
100
Methods of classes inheriting from SimpleGladeApp are used as
101
callbacks automatically.
104
an instance with methods as code of callbacks.
105
It means it has methods like on_button1_clicked, on_entry1_activate, etc.
107
self.glade.signal_autoconnect(callbacks_proxy)
109
def normalize_names(self):
111
It is internally used to normalize the name of the widgets.
112
It means a widget named foo:vbox-dialog in glade
113
is refered self.vbox_dialog in the code.
115
It also sets a data "prefixes" with the list of
116
prefixes a widget has for each widget.
118
for widget in self.get_widgets():
119
widget_name = gtk.Widget.get_name(widget)
120
prefixes_name_l = widget_name.split(":")
121
prefixes = prefixes_name_l[ : -1]
122
widget_api_name = prefixes_name_l[-1]
123
widget_api_name = "_".join( re.findall(tokenize.Name, widget_api_name) )
124
gtk.Widget.set_name(widget, widget_api_name)
125
if hasattr(self, widget_api_name):
126
raise AttributeError("instance %s already has an attribute %s" % (self,widget_api_name))
128
setattr(self, widget_api_name, widget)
130
gtk.Widget.set_data(widget, "prefixes", prefixes)
132
def add_prefix_actions(self, prefix_actions_proxy):
134
By using a gui designer (glade-2, gazpacho, etc)
135
widgets can have a prefix in theirs names
136
like foo:entry1 or foo:label3
137
It means entry1 and label3 has a prefix action named foo.
139
Then, prefix_actions_proxy must have a method named prefix_foo which
140
is called everytime a widget with prefix foo is found, using the found widget
143
prefix_actions_proxy:
144
An instance with methods as prefix actions.
145
It means it has methods like prefix_foo, prefix_bar, etc.
148
prefix_pos = len(prefix_s)
150
is_method = lambda t : callable( t[1] )
151
is_prefix_action = lambda t : t[0].startswith(prefix_s)
152
drop_prefix = lambda (k,w): (k[prefix_pos:],w)
154
members_t = inspect.getmembers(prefix_actions_proxy)
155
methods_t = filter(is_method, members_t)
156
prefix_actions_t = filter(is_prefix_action, methods_t)
157
prefix_actions_d = dict( map(drop_prefix, prefix_actions_t) )
159
for widget in self.get_widgets():
160
prefixes = gtk.Widget.get_data(widget, "prefixes")
162
for prefix in prefixes:
163
if prefix in prefix_actions_d:
164
prefix_action = prefix_actions_d[prefix]
165
prefix_action(widget)
167
#def custom_handler(self,
168
# glade, function_name, widget_name,
169
# str1, str2, int1, int2):
171
# Generic handler for creating custom widgets, internally used to
172
# enable custom widgets (custom widgets of glade).
174
# The custom widgets have a creation function specified in design time.
175
# Those creation functions are always called with str1,str2,int1,int2 as
176
# arguments, that are values specified in design time.
178
# Methods of classes inheriting from SimpleGladeApp are used as
179
# creation functions automatically.
181
# If a custom widget has create_foo as creation function, then the
182
# method named create_foo is called with str1,str2,int1,int2 as arguments.
185
# handler = getattr(self, function_name)
186
# return handler(str1, str2, int1, int2)
187
# except AttributeError:
190
def gtk_widget_show(self, widget, *args):
193
The widget is showed.
194
Equivalent to widget.show()
198
def gtk_widget_hide(self, widget, *args):
201
The widget is hidden.
202
Equivalent to widget.hide()
206
def gtk_widget_grab_focus(self, widget, *args):
209
The widget grabs the focus.
210
Equivalent to widget.grab_focus()
214
def gtk_widget_destroy(self, widget, *args):
217
The widget is destroyed.
218
Equivalent to widget.destroy()
222
def gtk_window_activate_default(self, window, *args):
225
The default widget of the window is activated.
226
Equivalent to window.activate_default()
228
widget.activate_default()
230
def gtk_true(self, *args):
233
Equivalent to return True in a callback.
234
Useful for stopping propagation of signals.
238
def gtk_false(self, *args):
241
Equivalent to return False in a callback.
245
def gtk_main_quit(self, *args):
248
Equivalent to self.quit()
254
Starts the main loop of processing events.
255
The default implementation calls gtk.main()
257
Useful for applications that needs a non gtk main loop.
258
For example, applications based on gstreamer needs to override
259
this method with gst.main()
261
Do not directly call this method in your programs.
262
Use the method run() instead.
268
Quit processing events.
269
The default implementation calls gtk.main_quit()
271
Useful for applications that needs a non gtk main loop.
272
For example, applications based on gstreamer needs to override
273
this method with gst.main_quit()
279
Starts the main loop of processing events checking for Control-C.
281
The default implementation checks wheter a Control-C is pressed,
282
then calls on_keyboard_interrupt().
284
Use this method for starting programs.
288
except KeyboardInterrupt:
289
self.on_keyboard_interrupt()
291
def on_keyboard_interrupt(self):
293
This method is called by the default implementation of run()
294
after a program is finished by pressing Control-C.
298
#def install_custom_handler(self, custom_handler):
299
# gtk.glade.set_custom_handler(custom_handler)
301
def create_glade(self, glade_path, root, domain):
302
return gtk.glade.XML(self.glade_path, root, domain)
304
def get_widget(self, widget_name):
305
return self.glade.get_widget(widget_name)
307
def get_widgets(self):
308
return self.glade.get_widget_prefix("")
310
# End copied material
312
class ConvenientImporter (importer.importer):
11
import gourmet.ImageExtras as ImageExtras
13
# 1. Make this interface actually import recipes...
14
# 2. Add drop-down menu buttons in place of red labels to make it
15
# trivial to change the label of something labelled.
16
# 3. Remove button-hell interface and come up with something cleaner
17
# and more attractive.
18
# 4. Add Undo support to editing (ugh -- this will be a PITA)
21
DEFAULT_TAG_LABELS = gglobals.REC_ATTR_DIC.copy()
22
for attr in gglobals.DEFAULT_ATTR_ORDER:
24
DEFAULT_TAGS.append(attr)
25
DEFAULT_TAGS.extend(['instructions','ingredients','inggroup','ignore'])
26
for tag,label in [('instructions',_('Instructions')),
27
('ingredients',_('Ingredients')),
28
('inggroup',_('Ingredient Subgroup')),
31
if not DEFAULT_TAG_LABELS.has_key(tag):
32
DEFAULT_TAG_LABELS[tag] = label
34
class ConvenientImporter (importer.Importer):
313
35
"""Add some convenience methods to our standard importer.
317
def autostart_rec (f):
318
"""A decorator to make sure our recipe has been started before
319
we add something to it.
321
def _ (self,*args,**kwargs):
322
self.set_added_to(True)
323
return f(self,*args,**kwargs)
326
37
def add_attribute (self, attname, txt):
327
self.set_added_to(True)
329
39
if self.rec.has_key(attname):
330
40
self.rec[attname] = self.rec[attname] + ', ' + txt
382
89
for i in txt.split(break_at): self.add_ing_from_text(i)
385
# Material copied from our generic recipe importer...
387
class InteractiveImporter (SimpleGladeApp, ConvenientImporter):
389
def __init__(self, rd, progress=None,
391
self.progress = progress
91
class InteractiveImporter (ConvenientImporter, NotThreadSafe):
93
NEW_REC_TEXT = _('New Recipe')
98
tag_labels=DEFAULT_TAG_LABELS,
392
100
if custom_parser: self.parser = custom_parser
393
101
else: self.parser = RecipeParser()
394
self.parser_to_choice = {
395
'ingredient':'Ingredient',
396
'ingredients':'Ingredients',
397
'instructions':'Instructions',
401
self.added_to = False
402
self.attdic = gglobals.REC_ATTR_DIC
403
self.textattdic = gglobals.TEXT_ATTR_DIC
404
SimpleGladeApp.__init__(self,
405
path=os.path.join(gglobals.gladebase,
406
"generic_importer.glade"),
409
ConvenientImporter.__init__(self,rd,threaded=True)
411
#-- InteractiveImporter.new {
413
self.get_widget('window1').show_all()
414
self.get_widget('window1').connect('delete-event',lambda *args: gtk.main_quit())
415
self.textview = self.glade.get_widget('textview')
416
self.textbuffer = self.textview.get_buffer()
417
self.textbuffer.connect('mark-set',self.on_cursor_moved)
418
#self.display = RecipeDisplay(self.glade)
419
self.glade.get_widget('window1').show()
420
self.populate_action_box()
421
#-- InteractiveImporter.new }
423
#-- InteractiveImporter custom methods {
424
# Write your own methods here
425
def populate_action_box (self):
426
"""Set up our choices of actions."""
427
self.action_box = self.glade.get_widget('treeview1')
428
self.action_to_label = {}
430
#'Instructions':self.display.add_instructions,
431
#'Notes':self.display.add_notes,
432
'Ingredient':lambda lab,txt: self.add_ing_from_text(txt),
433
'Ingredients': lambda lab,txt: self.add_ings_from_text(txt),
434
'Ingredient Subgroup':lambda lab,txt: self.add_ing_group(txt),
436
for attname,display_name in self.attdic.items():
437
self.actions[display_name] = self.add_attribute
438
self.action_to_label[display_name]=attname
439
for attname,display_name in self.textattdic.items():
440
self.actions[display_name] = self.add_text
441
self.action_to_label[display_name]=attname
442
keys = [self.attdic[a] for a in gglobals.DEFAULT_ATTR_ORDER]
443
keys.extend(['Ingredient Subgroup','Ingredient','Ingredients' ])
444
keys.extend([self.textattdic[a] for a in gglobals.DEFAULT_TEXT_ATTR_ORDER])
446
mod = gtk.ListStore(str)
447
for k in keys: mod.append([k])
448
# set up treeview column...
449
renderer = gtk.CellRendererText()
450
tvc = gtk.TreeViewColumn('',renderer,text=0)
451
self.action_box.append_column(tvc)
452
self.action_box.set_model(mod)
454
def get_current_action (self, *args):
455
"""Get the current default actions for our section of text."""
456
sel = self.action_box.get_selection().get_selected()
459
return mod.get_value(itr,0)
463
def set_current_action (self, action):
464
"""Set the current default action for our section of text."""
465
mod = self.action_box.get_model()
466
sel = self.action_box.get_selection()
469
sel.select_iter(r.iter)
471
# if we haven't returned by now, we have no action
474
def set_images (self, image_urls):
475
self.images = image_urls
477
def commit_rec (self, *args, **kwargs):
478
if hasattr(self,'images') and sys.platform!='win32':
479
if self.progress: self.progress(-1,_('Getting images...'))
480
self.ibd=imageBrowser.ImageBrowserDialog(
481
title=_("Select recipe image"),
482
label=_("Select recipe image."),
483
sublabel=_("Below are all the images found for the page you are importing. Select any images that are of the recipe, or don't select anything if you don't want any of these images."),
485
for i in self.images: self.ibd.add_image_from_uri(i)
489
fi=file(imageBrowser.get_image_file(image),'r')
490
self.rec['image']=fi.read()
492
ConvenientImporter.commit_rec(self,*args,**kwargs)
102
self.labels_by_tag = tag_labels
103
self.tags_by_label = {self.NEW_REC_TEXT:'newrec'}
104
for k,v in self.labels_by_tag.items(): self.tags_by_label[v]=k
107
self.setup_action_area()
108
self.markup_marks = {}; self.markup_partners = {}
110
self.midno = 0 # an ID counter for markup marks we insert
112
self.label_counts = {}
113
self.modal = modal # If we're in an embedded gtk mainloop...
114
ConvenientImporter.__init__(self)
116
def setup_window (self):
117
self.w = gtk.Window()
120
self.tv = gtk.TextView()
121
self.tv.set_size_request(600,400)
122
self.tv.set_wrap_mode(gtk.WRAP_WORD)
123
self.action_area = gtk.VBox()
124
sw = gtk.ScrolledWindow(); sw.add(self.tv)
125
sw.set_policy(gtk.POLICY_NEVER,gtk.POLICY_AUTOMATIC)
126
self.hb.add(sw); sw.show(); self.tv.show()
127
self.hb.add(self.action_area)
128
self.tb = self.tv.get_buffer()
131
def setup_action_area (self):
133
tag_button = gtk.Button('_'+self.labels_by_tag[t])
134
tag_button.connect('clicked',
136
self.labels_by_tag[t])
137
self.action_area.pack_start(tag_button,expand=False,fill=False,padding=6)
139
self.action_area.add(gtk.HSeparator())
140
self.remove_markup_button = gtk.Button(_('Clear _Tags'))
141
self.remove_markup_button.connect('clicked',self.clear_tags)
142
self.action_area.pack_start(self.remove_markup_button,expand=False,fill=False)
143
self.new_recipe_button = gtk.Button(_('_New Recipe'))
144
self.new_recipe_button.connect('clicked',self.new_recipe_cb)
145
self.action_area.pack_start(self.new_recipe_button)
146
self.action_area.pack_start(gtk.HSeparator(),expand=False,fill=False)
147
self.show_text_button = gtk.Button(stock=gtk.STOCK_OK)
148
self.show_text_button.connect('clicked',
149
lambda *args: self.commit_changes())
150
self.action_area.add(self.show_text_button)
151
self.action_area.show_all()
153
def setup_tags (self):
154
self.markup_tag = gtk.TextTag('markup')
155
self.markup_tag.set_property('editable',False)
156
self.markup_tag.set_property('scale',pango.SCALE_SMALL)
157
self.markup_tag.set_property('rise',15)
158
self.markup_tag.set_property('foreground',
161
self.ignore_tag = gtk.TextTag('ignore')
162
self.ignore_tag.set_property('invisible',True)
163
self.ignore_tag.set_property('editable',False)
164
self.tb.get_tag_table().add(self.markup_tag)
165
self.tb.get_tag_table().add(self.ignore_tag)
167
def label_callback (self, button, label):
168
self.label_selection(label)
170
def label_selection (self, label):
171
cursel = self.tb.get_selection_bounds()
175
# Otherwise, there's no clear sane default... we'll just
176
# select the current whole line
177
cur_pos = self.tb.get_insert()
178
cur_pos.backward_chars(
179
cur_pos.get_line_offset())
182
end = cur_pos.forward_line()
183
self.label_range(st,end,label)
185
def insert_with_label (self, st, text, label):
186
start_offset = st.get_offset()
187
self.tb.insert(st,text)
188
end_offset = start_offset + len(text)
190
self.tb.get_iter_at_offset(start_offset),
191
self.tb.get_iter_at_offset(end_offset),
195
def unhide_area (self, midno):
196
st,end = self.markup_marks[midno]
197
self.tb.remove_tag(self.ignore_tag,
198
self.tb.get_iter_at_mark(st),
199
self.tb.get_iter_at_mark(end)
202
def hide_range (self, st, end):
203
"""Hide text between start and end.
205
Return midno that can be used to unhide the range."""
206
midno = self.midno; self.midno += 1
207
start_mark = gtk.TextMark('start-markup-%s'%midno,False)
208
end_mark = gtk.TextMark('end-markup-%s'%midno,True)
209
self.tb.apply_tag(self.ignore_tag,
211
self.tb.add_mark(start_mark,st)
212
self.tb.add_mark(end_mark,end)
213
self.markup_marks[midno] = (start_mark,end_mark)
216
def label_range (self, st, end, label):
217
if self.tags_by_label.get(label,'')=='ignore':
218
midno = self.hide_range(st,end)
219
b = gtk.Button('Ignored text: Reveal hidden text')
220
anchor = self.insert_widget(end,b)
221
def unhide_text (*args):
222
self.unhide_area(midno)
223
self.remove_widget(anchor)
224
b.connect('clicked',unhide_text)
227
if self.label_counts.has_key(label):
228
count = self.label_counts[label]
229
self.label_counts[label] += 1
231
self.label_counts[label] = 1
233
smark = gtk.TextMark(label+'-'+str(count)+'-start',True)
234
emark = gtk.TextMark(label+'-'+str(count)+'-end',False)
235
self.tb.add_mark(smark,st)
236
self.tb.add_mark(emark,end)
237
self.labelled.append((smark,emark))
238
# Now we add the labels...
239
start_txt = '['+label+':'
240
start_id = self.insert_markup_text(st,start_txt,self.markup_tag)
241
# Now move the mark back up...
242
new_pos = self.tb.get_iter_at_mark(smark); new_pos.forward_chars(len(start_txt))
243
self.tb.move_mark(smark,new_pos)
244
# Create a "Remove me" button
245
itr = self.tb.get_iter_at_mark(emark)
246
#b = gtk.Button('_Remove tag'); b.show)(
248
i = gtk.Image(); i.set_from_stock(gtk.STOCK_REMOVE,gtk.ICON_SIZE_MENU)
250
anchor = self.insert_widget(itr,b)
251
# Add final bracket for end of markup
252
end_bracket_itr = self.tb.get_iter_at_mark(emark)
253
end_id = self.insert_markup_text(end_bracket_itr,']',self.markup_tag)
254
self.markup_partners[start_id]=end_id; self.markup_partners[end_id]=start_id
255
# Now back up our itr one character (it got advanced by adding
256
# the right bracket and the button)
257
eitr = self.tb.get_iter_at_mark(emark)
258
eitr.backward_chars(2)
259
self.tb.move_mark(emark,eitr)
260
# Define callback to remove our text when button is clicked
261
def remove_markup (*args):
262
self.labelled.remove((smark,emark))
263
self.remove_markup_text(start_id)
264
self.remove_markup_text(end_id)
265
self.remove_widget(anchor)
266
b.connect('clicked',remove_markup)
268
def new_recipe_cb (self, *args):
269
# Start a new recipe at cursor
270
itr = self.tb.get_iter_at_mark(self.tb.get_insert())
271
self.label_range(itr,itr,self.NEW_REC_TEXT)
273
def insert_markup_text (self, itr, text, *tags):
274
"""Insert markup text into the buffer. We do this in such a
275
way that we can remove it easily later.
277
midno = self.midno; self.midno += 1
278
start_mark = gtk.TextMark('start-markup-%s'%midno,False)
279
end_mark = gtk.TextMark('end-markup-%s'%midno,True)
280
start_offset = itr.get_offset()
282
self.tb.insert_with_tags(itr,text,*tags)
284
self.tb.insert(itr,text)
285
self.tb.add_mark(start_mark,self.tb.get_iter_at_offset(start_offset))
286
end_offset = start_offset + len(text)
287
end_itr = self.tb.get_iter_at_offset(end_offset)
288
self.tb.add_mark(end_mark,end_itr)
289
self.markup_marks[midno] = (start_mark,end_mark)
292
def insert_widget (self, itr, widget):
293
anchor = self.tb.create_child_anchor(itr)
294
self.anchors.append(anchor)
295
self.tv.add_child_at_anchor(widget,anchor)
296
widgetstart = self.tb.get_iter_at_child_anchor(anchor)
297
widgetend = widgetstart.copy(); widgetend.forward_char()
298
self.tb.apply_tag(self.markup_tag,widgetstart,widgetend)
302
def remove_widget (self, anchor):
303
anchor_iter = self.tb.get_iter_at_child_anchor(anchor)
304
delete_to = anchor_iter.copy()
305
delete_to.forward_char()
306
self.tb.delete(anchor_iter,delete_to)
308
def remove_markup_text (self, idno):
309
smark,emark = self.markup_marks[idno]
310
sitr,eitr = (self.tb.get_iter_at_mark(smark),
311
self.tb.get_iter_at_mark(emark))
312
self.tb.delete(sitr,eitr)
314
def clear_tags (self, *args):
315
"""Clear all markup in current selection, or whole buffer if
316
there is no selection
318
cursel = self.tb.get_selection_bounds()
322
st,end = self.tb.get_bounds()
323
st_offset = st.get_offset()
324
e_offset = end.get_offset()
325
for idno,iters in self.markup_marks.items():
327
if ((e_offset > self.tb.get_iter_at_mark(lst).get_offset() > st_offset)
329
(e_offset > self.tb.get_iter_at_mark(lend).get_offset() > st_offset)):
330
self.remove_markup_text(idno)
331
if self.markup_partners.has_key(idno):
332
self.remove_markup_text(self.markup_partners[idno])
333
for lst,lend in self.labelled[:]:
334
if ((e_offset > self.tb.get_iter_at_mark(lst).get_offset() > st_offset)
336
(e_offset > self.tb.get_iter_at_mark(lend).get_offset() > st_offset)):
337
self.labelled.remove((lst,lend))
338
for anchor in self.anchors[:]:
339
anchor_iter = self.tb.get_iter_at_child_anchor(anchor)
340
if e_offset > anchor_iter.get_offset() > st_offset:
341
self.anchors.remove(anchor)
342
self.remove_widget(anchor)
344
def commit_changes (self):
345
def mark_sorter (a,b):
346
a = self.tb.get_iter_at_mark(a[0]).get_offset()
347
b = self.tb.get_iter_at_mark(b[0]).get_offset()
349
self.labelled.sort(mark_sorter)
350
if not self.labelled: return
353
for smark,emark in self.labelled:
354
siter = self.tb.get_iter_at_mark(smark)
355
eiter = self.tb.get_iter_at_mark(emark)
356
text = siter.get_text(eiter)
357
name = smark.get_name()
358
label = name.split('-')[0]
359
print 'Processing',label,'->',text
360
tag = self.tags_by_label[label]
361
if tag in gglobals.TEXT_ATTR_DIC:
362
self.add_text(tag,text); started=True
363
elif tag in gglobals.REC_ATTR_DIC:
364
if text: self.add_attribute(tag,text)
365
elif tag == 'ingredient':
366
if text: self.add_ing_from_text(text); started=True
367
elif tag == 'ingredients':
368
if text: self.add_ings_from_text(text); started=True
369
elif tag == 'inggroup':
370
if text: self.add_ing_group(text); started=True
372
if not started: continue
373
# Then we're starting a new recipe at this point...
374
# Commit old recipe...
375
self.commit_rec(); started=False
381
print 'UNKNOWN TAG',tag,text,label
382
if started: self.commit_rec()
383
print "We've added the recipes...",self.added_recs
384
if hasattr(self,'images') and self.images:
385
# This is ugly -- we run the dialog once per recipe. This
386
# should happen rarely in current use-case (I don't know
387
# of a usecase where many recipes will come from a single
388
# text document / website); if in fact this becomes a
389
# common usecase, we'll need to rework the UI here.
390
for rec in self.added_recs:
391
ibd = imageBrowser.ImageBrowserDialog(
392
title=_('Select recipe image'),
393
label=_('Select image for recipe "%s"'%rec.title or _('Untitled')),
394
sublabel=_("Below are all the images found for the page you are importing. Select any images that are of the recipe, or don't select anything if you don't want any of these images."),
396
for i in self.images: ibd.add_image_from_uri(i)
399
ifi = file(imageBrowser.get_image_file(ibd.ret),'r')
400
image_str = ifi.read(); ifi.close()
401
image = ImageExtras.get_image_from_string(image_str)
403
thumb = ImageExtras.resize_image(image,40,40)
404
self.rd.modify_rec(rec,{'image':ImageExtras.get_string_from_image(image),
405
'thumb':ImageExtras.get_string_from_image(thumb),
494
411
def set_text (self, txt):
498
raise ValueError("There is no text to set")
499
self.textbuffer = gtk.TextBuffer()
500
self.textview.set_buffer(self.textbuffer)
501
parsed = self.parser.parse(txt,progress=self.progress)
502
tagtable = self.textbuffer.get_tag_table()
507
for line,tag in parsed:
509
self.progress(float(n)/tot,
510
_('Setting up interactive importer')
412
txt = unicode(txt) # convert to unicode for good measure
413
self.set_parsed(self.parser.parse(txt))
416
def set_parsed (self, parsed):
417
for chunk,tag in parsed:
514
self.textbuffer.insert(self.textbuffer.get_end_iter(),
419
self.tb.insert(self.tb.get_end_iter(),
517
if not tagtable.lookup(tag):
518
tagtable.add(gtk.TextTag(tag))
519
smark = self.textbuffer.create_mark(None,
520
self.textbuffer.get_end_iter(),
522
self.textbuffer.insert_with_tags_by_name(
523
self.textbuffer.get_end_iter(),
422
self.insert_with_label(
423
self.tb.get_end_iter(),
425
self.labels_by_tag.get(tag,tag)
527
emark = self.textbuffer.create_mark(None,
528
self.textbuffer.get_end_iter(),
530
self.sections.append((smark,emark))
534
def goto_next_section (self):
535
"""Goto our next section"""
536
cur_sec = self.get_section_containing_mark()
537
end_bound =self.textbuffer.get_iter_at_mark(
538
self.sections[cur_sec][1]
540
cur_pos = self.textbuffer.get_iter_at_mark(
541
self.textbuffer.get_insert()
543
if cur_pos < end_bound:
544
self.goto_section(cur_sec,direction=1)
546
self.goto_section(cur_sec+1,direction=1)
548
def goto_prev_section (self):
549
"""Goto our previous section"""
550
cur_sec = self.get_section_containing_mark()
551
start_bound =self.textbuffer.get_iter_at_mark(
552
self.sections[cur_sec][0]
554
cur_sel = self.textbuffer.get_selection_bounds()
556
cur_pos = (cur_sel[0].get_offset()<cur_sel[1].get_offset() and
557
cur_sel[0].get_offset() or
558
cur_sel[1].get_offset()
561
cur_pos = self.textbuffer.get_iter_at_mark(
562
self.textbuffer.get_insert()
564
if cur_pos > start_bound:
565
self.goto_section(cur_sec,direction=-1)
567
self.goto_section(cur_sec-1,direction=-1)
569
def section_contains_mark (self, section, mark=None):
570
if not mark: mark = self.textbuffer.get_insert()
571
if type(section)==int: section = self.sections[section]
573
siter = self.textbuffer.get_iter_at_mark(st)
574
eiter = self.textbuffer.get_iter_at_mark(end)
575
citer = self.textbuffer.get_iter_at_mark(mark)
576
return siter.get_offset() < citer.get_offset() < eiter.get_offset()
578
def get_section_containing_mark (self):
579
"""Get the current position of our cursor relative to our marks"""
580
itr = self.textbuffer.get_iter_at_mark(self.textbuffer.get_insert())
581
cur_offset = itr.get_offset()
582
for n,mks in enumerate(self.sections):
583
start = self.textbuffer.get_iter_at_mark(mks[0])
584
end = self.textbuffer.get_iter_at_mark(mks[1])
585
if start.get_offset() <= cur_offset < end.get_offset():
587
elif cur_offset < start.get_offset():
589
return len(self.sections)-1
591
def goto_section (self, n, direction=1):
592
"""Move to section numbered n
594
If direction is positive, adjust forward if necessary.
595
If direction is negative, adjust backward if necessary.
597
if n >= len(self.sections): n = len(self.sections)-1
600
if len(self.sections)-1 <= n:
601
print "There is no section ",n
604
start_itr=self.textbuffer.get_iter_at_mark(s)
605
end_itr = self.textbuffer.get_iter_at_mark(e)
606
# Check where our current section is
607
cur_sel = self.textbuffer.get_selection_bounds()
609
soffset,eoffset = start_itr.get_offset(),end_itr.get_offset()
610
cur_start,cur_end = cur_sel
611
if cur_start.get_offset() < soffset and cur_end.get_offset() > eoffset:
612
if direction>0: self.goto_section(n+1)
613
else: self.goto_section(n-1)
614
if direction > 0 and soffset < cur_end.get_offset() < eoffset:
617
if re.match('\s',start_itr.get_char()):
618
start_itr.forward_find_char(lambda c,user_data: re.match('\S',c) and True,
620
elif direction < 0 and soffset < cur_start.get_offset() < eoffset:
622
if re.match('\s',start_itr.get_char()):
623
start_itr.backward_find_char(lambda c,user_data: re.match('\S',c) and True,
625
#if end_itr.get_offset()==start_itr.get_offset(): print 'Funny, end == start'
626
self.textbuffer.select_range(end_itr,
629
self.textview.scroll_to_iter(start_itr,0.3)
630
self.on_cursor_moved(self.textview)
431
self.w.connect('delete-event',gtk.main_quit)
434
self.w.connect('delete-event',lambda *args: self.w.hide())
633
#-- InteractiveImporter custom methods }
635
#-- InteractiveImporter.on_open {
636
def on_open(self, widget, *args):
637
fname = dialog_extras.select_file('Open recipe',
638
filters=[['Plain Text',['text/plain'],'*.txt']])
640
ofi = file(fname,'r')
641
self.set_text(ofi.read())
643
#-- InteractiveImporter.on_open }
645
#-- InteractiveImporter.on_open_url {
646
def on_open_url(self, widget, *args):
647
# A quick hack to try web import... eventually we'll want to
648
# use urllib to do something crossplatform and reasonable if
649
# we want this (and of course we can just borrow code from
650
# Gourmet which already does this right)
651
url = dialog_extras.getEntry(label='Enter address of webpage with a recipe on it.',
653
entryTip="""URLs start with http://""")
654
if url.find('//')<0: url = 'http://'+url
655
ifi = os.popen('w3m -T text/html -dump %(url)s'%locals())
656
self.set_text(ifi.read())
657
#-- InteractiveImporter.on_open_url }
659
#-- InteractiveImporter.on_save {
660
def on_save(self, widget, *args):
663
#-- InteractiveImporter.on_quit {
664
def on_quit(self, widget, *args):
666
self.glade.get_widget('window1').hide()
667
if self.progress: self.progress(1,_('Import complete!'))
670
#-- InteractiveImporter.on_quit }
672
def set_added_to (self, bool):
673
"""Set a switch that we have been added to, or not."""
675
if bool: self.glade.get_widget('NewRecipeButton').set_sensitive(bool)
677
def on_new_recipe (self, *args):
678
# If we already have a recipe
682
self.set_added_to(False)
684
#-- InteractiveImporter.on_cursor_moved {
685
def on_cursor_moved (self, widget, *args):
686
cursor = self.textbuffer.get_insert()
687
itr = self.textbuffer.get_iter_at_mark(cursor)
688
tags = itr.get_tags()
690
itr = self.textbuffer.get_iter_at_offset(
693
tags = itr.get_tags()
696
if self.parser_to_choice.has_key(t.get_property('name')):
697
action = self.parser_to_choice[t.get_property('name')]
698
elif self.attdic.has_key(t.get_property('name')):
699
action = self.attdic[t.get_property('name')]
700
elif self.textattdic.has_key(t.get_property('name')):
701
action = self.textattdic[t.get_property('name')]
702
self.set_current_action(action)
704
#-- InteractiveImporter.on_cursor_moved }
706
#-- InteractiveImporter.on_ingredientEventBox_drag_drop {
707
def on_ingredientEventBox_drag_drop(self, widget, *args):
709
#-- InteractiveImporter.on_ingredientEventBox_drag_drop }
711
#-- InteractiveImporter.on_back {
712
def on_back(self, widget, *args):
713
self.goto_prev_section()
714
#-- InteractiveImporter.on_back }
716
#-- InteractiveImporter.on_forward {
717
def on_forward(self, widget, *args):
718
self.goto_next_section()
719
#-- InteractiveImporter.on_forward }
721
#-- InteractiveImporter.on_apply {
722
def on_apply(self, widget, *args):
723
selection = self.textbuffer.get_text(
724
*self.textbuffer.get_selection_bounds()
726
active_txt = self.get_current_action()
727
if not hasattr(self,'inserted_tag'):
728
self.inserted_tag = gtk.TextTag('inserted')
729
self.inserted_tag.set_property('editable',False)
730
self.inserted_tag.set_property('style',pango.STYLE_ITALIC)
731
if not self.textbuffer.get_tag_table().lookup('inserted'):
732
self.textbuffer.get_tag_table().add(self.inserted_tag)
733
self.textbuffer.apply_tag(self.inserted_tag,*self.textbuffer.get_selection_bounds())
734
if not hasattr(self,'markup_tag'):
735
self.markup_tag = gtk.TextTag('markup')
736
self.markup_tag.set_property('editable',False)
737
self.markup_tag.set_property('scale',pango.SCALE_SMALL)
738
self.markup_tag.set_property('rise',15)
739
self.markup_tag.set_property('foreground',
742
if not self.textbuffer.get_tag_table().lookup('markup'):
743
self.textbuffer.get_tag_table().add(self.markup_tag)
744
st,end = self.textbuffer.get_selection_bounds()
745
self.textbuffer.insert_with_tags(st,'['+active_txt+':',self.markup_tag)
746
st,end = self.textbuffer.get_selection_bounds()
747
while end.starts_line() and end.backward_char():
748
end = self.textbuffer.get_iter_at_offset(end.get_offset()-1)
750
# Here's how we would add a "Remove" button -- but this really
751
# requires rethinking the whole process...
753
#anchor = self.textbuffer.create_child_anchor(end)
754
#button = gtk.Button('_Remove tag'); button.show()
755
#self.textview.add_child_at_anchor(button,anchor)
757
self.textbuffer.insert_with_tags(end,']',self.markup_tag)
758
action = self.actions[active_txt]
759
if self.action_to_label.has_key(active_txt):
760
active_txt = self.action_to_label[active_txt]
761
action(active_txt,selection)
762
self.on_forward(None)
763
#-- InteractiveImporter.on_apply }
765
class InteractiveTextImporter (InteractiveImporter):
766
def __init__ (self, filename, rd, progress=None, source=None, threaded=False,custom_parser=None):
767
InteractiveImporter.__init__(self,rd,progress=progress,custom_parser=custom_parser)
768
ofi = file(filename,'r')
770
'\n'.join(check_encodings.get_file(ofi)) # this is mildly idiotic
772
#self.set_text(ofi.read())
775
def __repr__ (self): return "<InteractiveTextImporter>"
778
from gourmet import recipeManager
779
rd = recipeManager.RecipeManager(file='/tmp/foo.db')
780
window1 = InteractiveImporter(rd)
438
if __name__ == '__main__':
439
ii = InteractiveImporter()
440
ii.w.connect('delete-event',gtk.main_quit)
443
ii.images = ['http://grecipe-manager.sourceforge.net/CardView.png']
783
446
Quick Pesto Dinner