1
2
3 '''
4 GTK UI classes for metapho: an image tagger and viewer.
5 '''
6
7
8
9 from . import metapho
10
11 import gtk
12 import gc
13 import glib, gobject
14 import os
15 import collections
16
18 '''A PyGTK widget for showing tags.
19 '''
21 metapho.Tagger.__init__(self)
22 self.num_rows = 26
23 gtk.Table.__init__(self, 4, self.num_rows, False)
24
25 self.parentwin = parentwin
26
27 self.title = gtk.Label("Tags")
28 self.attach(self.title, 0, 4, 0, 1 );
29
30 hbox = gtk.HBox()
31 hbox.pack_start(gtk.Label("Tag category:"), expand=False)
32
33 edit_btn = gtk.Button("Edit")
34 edit_btn.connect("clicked", self.edit_categories)
35 hbox.pack_end(edit_btn, expand=False)
36 edit_btn.unset_flags(gtk.CAN_FOCUS)
37
38
39
40
41
42
43 self.cat_list_store = gtk.ListStore(str)
44 self.categorysel = gtk.ComboBox(self.cat_list_store)
45
46 cr = gtk.CellRendererText()
47 self.categorysel.pack_start(cr)
48 self.categorysel.set_attributes(cr, text=0)
49
50 self.categorysel.unset_flags(gtk.CAN_FOCUS)
51
52 hbox.pack_start(self.categorysel, expand=True)
53
54 self.attach(hbox, 0, 4, 1, 2 );
55
56 self.cur_img = None
57 self.highlight_bg = gtk.gdk.color_parse("#FFFFFF")
58 self.greyBG = gtk.gdk.color_parse("#DDDDDD")
59 self.matchBG = gtk.gdk.color_parse("#DDFFEE")
60 self.ignore_events = False
61
62 self.editing = False
63
64
65 self.buttons = []
66 self.entries = []
67 self.button_names = []
68 for j in range(0, 2) :
69 for i in range(0, self.num_rows) :
70 if j <= 0 :
71 buttonchar = chr(i + ord('a'))
72 left = 0
73 else :
74 buttonchar = chr(i + ord('A'))
75 left = 2
76
77 button = gtk.ToggleButton(buttonchar)
78 self.attach(button, left, left+1, i+2, i+3 );
79 self.buttons.append(button)
80 button.connect("toggled", self.toggled, len(self.entries))
81
82 entry = gtk.Entry()
83 entry.set_width_chars(25)
84
85
86 entry.connect("focus-out-event", self.focus_out,
87 len(self.entries))
88 self.attach(entry, left+1, left+2, i+2, i+3 );
89 self.entries.append(entry)
90
91 self.show()
92
94 '''Update a tag: called on focus_out from one of the text entries'''
95 if tagno < len(self.categories[self.current_category]) :
96 self.tag_list[self.categories[self.current_category][tagno]] = newstr
97 else :
98 newtag = self.add_tag(newstr, self.cur_img)
99 self.highlight_tag(newtag, True)
100
110
112 '''Check whether any entries are empty.
113 If so, make sure they're unhighlighted.
114 '''
115 for i, ent in enumerate(self.entries) :
116 if self.buttons[i].get_active() and not ent.get_text() :
117 self.highlight_tag(i, False)
118
120 '''Un-focus any currently focused text entry,
121 leaving nothing focused.
122 If there was a focused entry and it was empty,
123 de-select the corresponding toggle button.
124 '''
125 focused = self.parentwin.get_focus()
126
127
128
129
130
131
132
133
134 self.unhighlight_empty_entries()
135 self.parentwin.set_focus(None)
136
138 entry_text = entry.get_text()
139
140 if entry_text.strip() == '' :
141 return
142 self.change_tag(tagno, entry_text)
143 return True
144
145 - def toggled(self, button, tagno) :
146
147
148
149 if self.ignore_events :
150 return
151
152
153 if button.get_active() :
154
155 self.add_tag(tagno, self.cur_img)
156 else :
157
158 self.remove_tag(tagno, self.cur_img)
159
160
161
162
163 self.focus_none()
164
165 return True
166
167 - def check_entry_tag(focused_widget) :
168 '''At certain times, such as just before exit, the main window
169 may call us to alert us that a tag may have changed.
170 We need to find out which entry contains it and check the tag.
171 '''
172 for i, ent in enumerate(self.entries) :
173 if focused_widget == ent :
174 self.focus_out(ent, None, i)
175
195
229
231 '''The callback when the combobox is changed by the user'''
232 self.display_tags_for_category(combobox.get_active_text())
233
235 '''Advance to the next category (if howmany==1) or some other category.
236 '''
237 keys = self.categories.keys()
238 catno = keys.index(self.current_category)
239 catno = (catno + howmany) % len(keys)
240 self.show_category_by_number(catno)
241
243 '''Show a specific category by number.
244 Raises IndexError if catno is out of range.
245 '''
246 self.display_tags_for_category(self.categories.keys()[catno])
247 self.categorysel.set_active(catno)
248
250 d = gtk.Dialog('Edit categories', self.parentwin,
251 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE,
252 gtk.STOCK_OK, gtk.RESPONSE_OK))
253 d.set_default_size(300, 500)
254
255 v = gtk.VBox()
256
257 self.edit_cell = None
258 self.edit_path = None
259
260 def category_edited(cr, path, text):
261 self.cat_list_store[path][0] = text
262
263 def get_cell(cr, editable, path) :
264 self.edit_cell = editable
265 self.edit_path = path
266
267 t = gtk.TreeView(self.cat_list_store)
268 cr = gtk.CellRendererText()
269 cr.props.editable = True
270 cr.connect('edited', category_edited)
271 cr.connect('editing_started', get_cell)
272
273
274
275 col = gtk.TreeViewColumn('Category', cr, text=0)
276 t.insert_column(col, -1)
277
278 sw = gtk.ScrolledWindow()
279 sw.add(t)
280 v.add(sw)
281
282 def save_current() :
283 if self.edit_cell and self.edit_path :
284 self.cat_list_store[self.edit_path][0] = \
285 self.edit_cell.get_text()
286
287 def add_category(b):
288 save_current()
289 self.cat_list_store.append(('New category',))
290 t.set_cursor(self.cat_list_store[-1].path, col, True)
291
292 b = gtk.Button('Add...')
293 b.connect('clicked', add_category)
294 v.pack_start(b, False, False)
295
296 d.get_content_area().add(v)
297
298 d.show_all()
299
300 while gtk.events_pending():
301 gtk.main_iteration(False)
302
303 response = d.run()
304 if response == gtk.RESPONSE_OK :
305 save_current()
306
307
308
309
310
311 oldkeys = self.categories.keys()
312 old_cur_pos = oldkeys.index(self.current_category)
313 i = 0
314
315 newcats = collections.OrderedDict()
316
317
318
319 iter = self.cat_list_store.get_iter_first()
320 while iter:
321 item = self.cat_list_store.get_value(iter, 0)
322 if i < len(oldkeys) :
323 newcats[item] = self.categories[oldkeys[i]]
324 else :
325 newcats[item] = []
326 if i == old_cur_pos :
327 self.current_category = item
328 iter = self.cat_list_store.iter_next(iter)
329 i += 1
330
331 self.categories = newcats
332 if self.current_category > i :
333 self.current_category = 0
334 self.show_category_by_number(self.current_category)
335
336 d.destroy()
337
338 self.focus_none()
339
341 '''Turn tag number tagno on (if val=True) or off (val=False).'''
342
343 if len(self.buttons) < tagno :
344 print "Argh! Tried to highlight tag", tagno
345 if self.buttons[tagno].get_active() != val :
346 self.ignore_events = True
347 self.buttons[tagno].set_active(val)
348 self.ignore_events = False
349
350 if val :
351 self.entries[tagno].modify_base(gtk.STATE_NORMAL, self.highlight_bg)
352
353
354 if not self.entries[tagno].get_text().strip() :
355 self.parentwin.set_focus(self.entries[tagno])
356 else :
357 self.entries[tagno].modify_base(gtk.STATE_NORMAL, self.greyBG)
358 if self.parentwin.get_focus() == self.entries[tagno] :
359 self.focus_none()
360
362 '''Colorize any tags that match the given pattern.
363 If pat == None, un-colorize everything.
364 '''
365 if pat :
366 self.title.set_text("search: " + pat)
367 else :
368 self.title.set_text(os.path.basename(self.cur_img.filename))
369 pat = pat.lower()
370 for i, ent in enumerate(self.entries) :
371 if pat and (ent.get_text().lower().find(pat) >= 0) :
372 ent.modify_base(gtk.STATE_NORMAL, self.matchBG)
373 elif self.buttons[i].get_active() :
374 ent.modify_base(gtk.STATE_NORMAL, self.highlight_bg)
375 else :
376 ent.modify_base(gtk.STATE_NORMAL, self.greyBG)
377
379 '''Focus the first text field matching the pattern.'''
380 self.title.set_text(os.path.basename(self.cur_img.filename))
381 pat = pat.lower()
382 for i, ent in enumerate(self.entries) :
383 if pat and (ent.get_text().lower().find(pat) >= 0) :
384 self.buttons[i].set_active(True)
385 ent.modify_base(gtk.STATE_NORMAL, self.matchBG)
386 return
387
389 self.cur_img = img
390
391 self.title.set_text(os.path.basename(img.filename))
392
393 self.display_tags_for_category(self.current_category)
394
395 return
396
398 '''Add a tag to the given image.
399 img is a metapho.Image.
400 tag may be a string, which can be a new string or an existing one,
401 or an integer index into the tag list.
402 Return the index (in the global tags list) of the tag just added,
403 or None if error.
404 '''
405
406 tagindex = metapho.Tagger.add_tag(self, tag, img)
407
408
409 if tagindex < len(self.entries) :
410 self.highlight_tag(tagindex, True)
411
412 return tagindex
413
415 if not type(tag) is int :
416 tagstr = tag
417 tag = self.tag_list.index(tagstr)
418 if tagstr < 0 :
419 print "No such tag", tagstr
420 return
421
422 metapho.Tagger.remove_tag(self, tag, img)
423
424 self.highlight_tag(tag, False)
425
427 '''Toggle tag number tagno for the given img.'''
428 metapho.Tagger.toggle_tag(self, tagno, img)
429 if tagno < len(self.categories[self.current_category]) :
430 self.highlight_tag(tagno, not self.buttons[tagno].get_active())
431
433 '''Toggle the tag corresponding to the letter typed by the user'''
434 if tagchar.islower() :
435 tagno = ord(tagchar) - ord('a')
436 else :
437 tagno = ord(tagchar) - ord('A') + self.num_rows
438 if tagno >= len(self.tag_list) :
439 print "We don't have a tag", tagchar, "yet"
440 return
441 self.toggle_tag(tagno, img)
442
443 - def focus_next_entry(self) :
444 '''Set focus to the next available entry.
445 If we're already typing in a new tag entry that hasn't been
446 saved yet, save it first before switching to the new one.
447 '''
448 newindex = len(self.categories[self.current_category])
449
450
451
452
453
454
455 curtext = self.entries[newindex].get_text()
456 if curtext.strip() != '' :
457 newindex += 1
458
459 self.parentwin.set_focus(self.entries[newindex])
460 self.highlight_tag(newindex, True)
461
463 '''A PyGTK image viewer widget for metapho.
464 '''
465
467 super(ImageViewer, self).__init__()
468 self.connect("expose-event", self.expose_handler)
469 self.gc = None
470 self.pixbuf = None
471 self.imgwidth = None
472 self.imgheight = None
473 self.cur_img = None
474
476
477
478 if not self.gc :
479 self.gc = widget.window.new_gc()
480 x, y, self.imgwidth, self.imgheight = self.get_allocation()
481
482
483
484 if self.cur_img and not self.pixbuf :
485 self.load_image(self.cur_img)
486
487 self.show_image()
488
489
490
491 exif_rot_table = [ 0, 0, 180, 180, 270, 270, 90, 90 ]
492
493
494
495
497 '''Load the image passed in, and show it.
498 img is a metapho.Image object.
499 Return True for success, False for error.
500 '''
501
502 self.cur_img = img
503
504
505
506 if self.pixbuf :
507 self.pixbuf = None
508
509 try :
510 newpb = gtk.gdk.pixbuf_new_from_file(img.filename)
511
512
513
514
515
516
517
518
519 if not self.imgwidth :
520 return True
521
522
523 if img.rot == None :
524
525 orient = newpb.get_option('orientation')
526 if orient == None :
527 orient = 0
528 else :
529 orient = int(orient) - 1
530 img.rot = self.exif_rot_table[orient]
531
532
533
534
535 oldw = newpb.get_width()
536 oldh = newpb.get_height()
537 if img.rot in [ 0, 180] :
538 if oldw > oldh :
539 neww = self.imgwidth
540 newh = oldh * self.imgwidth / oldw
541 else :
542 newh = self.imgheight
543 neww = oldw * self.imgheight / oldh
544
545
546
547
548
549 else :
550 if oldw > oldh :
551 neww = self.imgheight
552 newh = oldh * self.imgheight / oldw
553 else :
554 neww = self.imgwidth
555 newh = oldh * self.imgwidth / oldw
556
557
558 newpb = newpb.scale_simple(neww, newh,
559 gtk.gdk.INTERP_BILINEAR)
560
561
562 if img.rot != 0 :
563 newpb = newpb.rotate_simple(img.rot)
564
565
566
567 self.pixbuf = newpb
568
569 self.show_image()
570 loaded = True
571
572 except glib.GError :
573 self.pixbuf = None
574 loaded = False
575
576
577 newpb = None
578 gc.collect()
579
580 return loaded
581
583 if not self.gc :
584 return
585
586 if not self.pixbuf :
587 return
588
589
590 self.window.draw_rectangle(self.gc, True, 0, 0,
591 self.imgwidth, self.imgheight)
592
593 x = (self.imgwidth - self.pixbuf.get_width()) / 2
594 y = (self.imgheight - self.pixbuf.get_height()) / 2
595 self.window.draw_pixbuf(self.gc, self.pixbuf, 0, 0, x, y)
596
598 self.cur_img.rot = (self.cur_img.rot + rot + 360) % 360
599
600
601 self.load_image(self.cur_img)
602