If picture does no exist, warn and exit
[cascardo/movie.git] / gzv.py
1 # -*- coding: utf-8; -*-
2 # gzv.py - an user interface to select people in a picture
3 #
4 # Copyright (C) 2008  Lincoln de Sousa <lincoln@minaslivre.org>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 import os
17 import gtk
18 import gtk.glade
19 import gobject
20 import math
21 import cairo
22 from ConfigParser import ConfigParser
23
24 _ = lambda x:x
25
26 class Point(object):
27     def __init__(self, x, y):
28         self.x = x
29         self.y = y
30
31     @staticmethod
32     def pythagorean(p1, p2):
33         return math.sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2)
34
35 class Ball(object):
36     DEFAULT_WIDTH = 10
37
38     def __init__(self, x, y, r, name='', position=0, selected=False):
39         self.position = position
40         self.selected = selected
41         self.p = Point(int(x), int(y))
42         self.radius = int(r)
43         self.name = name
44
45 class BallManager(list):
46     def __init__(self, *args, **kwargs):
47         super(BallManager, self).__init__(*args, **kwargs)
48
49     def save_to_file(self, path):
50         target = open(path, 'w')
51         for i in self:
52             target.write('%d,%d %d %s\n' % (i.p.x, i.p.y, i.radius, i.name))
53         target.close()
54
55 class GladeLoader(object):
56     def __init__(self, fname, root=''):
57         self.ui = gtk.glade.XML(fname, root)
58         self.ui.signal_autoconnect(self)
59
60     def get_widget(self, wname):
61         return self.ui.get_widget(wname)
62
63     # little shortcut
64     wid = get_widget
65
66     # glade callbacks
67
68     def gtk_widget_show(self, widget, *args):
69         widget.show()
70         return True
71
72     def gtk_widget_hide(self, widget, *args):
73         widget.hide()
74         return True
75
76     def gtk_main_quit(self, *args):
77         gtk.main_quit()
78
79     def gtk_main(self, *args):
80         gtk.main()
81
82 class Project(object):
83     def __init__(self, image, width, height):
84         self.image = image
85         self.width = width
86         self.height = height
87         self.focus_points_file = ''
88
89     def save_to_file(self, path):
90         if not self.focus_points_file:
91             bn = os.path.basename(path)
92             name = os.path.splitext(bn)[0]
93             self.focus_points_file = \
94                 os.path.join(os.path.dirname(path), name + '_fpf')
95
96         cp = ConfigParser()
97         cp.add_section('Project')
98         cp.set('Project', 'image', self.image)
99         cp.set('Project', 'width', self.width)
100         cp.set('Project', 'height', self.height)
101         cp.set('Project', 'focus_points', self.focus_points_file)
102         
103         cp.write(open(path, 'w'))
104
105     @staticmethod
106     def parse_file(path):
107         cp = ConfigParser()
108         cp.read(path)
109
110         image = cp.get('Project', 'image')
111         width = cp.getint('Project', 'width')
112         height = cp.getint('Project', 'height')
113         x = cp.getint('Project', 'height')
114
115         proj = Project(image, width, height)
116         proj.focus_points_file = cp.get('Project', 'focus_points')
117
118         return proj
119
120 class NewProject(GladeLoader):
121     def __init__(self, parent=None):
122         super(NewProject, self).__init__('gzv.glade', 'new-project')
123         self.dialog = self.wid('new-project')
124         if parent:
125             self.dialog.set_transient_for(parent)
126
127     def get_project(self):
128         # This '1' was defined in the glade file
129         if not self.dialog.run() == 1:
130             return None
131
132         fname = self.wid('image').get_filename()
133         width = self.wid('width').get_text()
134         height = self.wid('height').get_text()
135         return Project(fname, width, height)
136
137     def destroy(self):
138         self.dialog.destroy()
139
140 class Gzv(GladeLoader):
141     def __init__(self):
142         super(Gzv, self).__init__('gzv.glade', 'main-window')
143         self.window = self.wid('main-window')
144         self.window.connect('delete-event', lambda *x: gtk.main_quit())
145
146         self.evtbox = self.wid('eventbox')
147         self.evtbox.connect('button-press-event', self.button_press)
148         self.evtbox.connect('button-release-event', self.button_release)
149         self.evtbox.connect('motion-notify-event', self.motion_notify)
150         self.evtbox.connect('motion-notify-event', self.ball_motion)
151
152         # making it possible to grab motion events when the mouse is
153         # over the widget.
154         self.evtbox.set_events(gtk.gdk.POINTER_MOTION_MASK)
155
156         self.model = gtk.ListStore(int, str)
157         self.treeview = self.wid('treeview')
158         self.treeview.set_model(self.model)
159         self.treeview.connect('button-press-event', self.select_fp)
160
161         self.draw = self.wid('draw')
162         self.draw.connect_after('expose-event', self.expose_draw)
163
164         # Starting with an empty project with no image loaded
165         self.project = None
166         self.image = None
167
168         # This attr may be overriten, if so, call the method (load_balls_to_treeview)
169         self.balls = BallManager()
170
171         self.load_balls_to_treeview()
172         self.setup_treeview()
173
174         self.new_ball = False
175         self.move_ball = None
176
177         # drawing stuff
178         self.start_x = -1
179         self.start_y = -1
180         self.last_x = -1
181         self.last_y = -1
182         self.radius = Ball.DEFAULT_WIDTH
183
184     def show(self):
185         self.window.show_all()
186
187     def setup_treeview(self):
188         self.model.connect('rows-reordered', self.on_rows_reordered)
189
190         renderer = gtk.CellRendererText()
191         column = gtk.TreeViewColumn(_('Position'), renderer, text=0)
192         column.set_property('visible', False)
193         self.treeview.append_column(column)
194
195         renderer = gtk.CellRendererText()
196         renderer.connect('edited', self.on_cell_edited)
197         renderer.set_property('editable', True)
198         self.fpcolumn = gtk.TreeViewColumn(_('Name'), renderer, text=1)
199         self.treeview.append_column(self.fpcolumn)
200
201     def on_rows_reordered(self, *args):
202         print 
203
204     def on_cell_edited(self, renderer, path, value):
205         self.balls[int(path)].name = value
206         self.load_balls_to_treeview()
207
208     def new_project(self, button):
209         proj = NewProject(self.window)
210         project = proj.get_project()
211         proj.destroy()
212
213         if project:
214             self.load_project(project)
215
216     def open_project(self, *args):
217         fc = gtk.FileChooserDialog(_('Choose a gzv project'), self.window,
218                                    buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
219                                             gtk.STOCK_OK, gtk.RESPONSE_OK))
220         if fc.run() == gtk.RESPONSE_OK:
221             proj_file = fc.get_filename()
222             self.load_project(Project.parse_file(proj_file))
223         fc.destroy()
224
225     def save_project(self, *args):
226         fc = gtk.FileChooserDialog(_('Save project'), self.window,
227                                    action=gtk.FILE_CHOOSER_ACTION_SAVE,
228                                    buttons=(gtk.STOCK_CANCEL,
229                                             gtk.RESPONSE_CANCEL,
230                                             gtk.STOCK_SAVE,
231                                             gtk.RESPONSE_OK))
232         if fc.run() == gtk.RESPONSE_OK:
233             self.project.save_to_file(fc.get_filename())
234             self.balls.save_to_file(self.project.focus_points_file)
235         fc.destroy()
236
237     def load_project(self, project):
238         self.project = project
239         self.balls = self.load_balls_from_file(project.focus_points_file)
240         self.image = project.image
241
242         # I'm loading a pixbuf first because I need to get its
243         # dimensions this with a pixbuf is easier than with an image.
244         try:
245             pixbuf = gtk.gdk.pixbuf_new_from_file(project.image)
246         except gobject.GError:
247             msg = _("Couldn't recognize the image file format.")
248             dialog = gtk.MessageDialog(self.window,
249                                        gtk.DIALOG_MODAL,
250                                        gtk.MESSAGE_ERROR,
251                                        gtk.BUTTONS_CLOSE)
252             dialog.set_markup(msg)
253             dialog.run()
254             dialog.destroy()
255             return self.unload_project()
256
257         self.draw.set_from_pixbuf(pixbuf)
258         self.load_balls_to_treeview()
259         self.set_widgets_sensitivity(True)
260
261     def unload_project(self):
262         self.project = None
263         self.image = None
264         self.balls = BallManager()
265         self.draw.queue_draw()
266         self.set_widgets_sensitivity(False)
267
268     def set_widgets_sensitivity(self, sensitive):
269         for i in 'toolbutton1', 'toolbutton5', 'scrolledwindow1', \
270                 'hbox2', 'imagemenuitem3':
271             self.wid(i).set_sensitive(sensitive)
272
273     def load_balls_to_treeview(self):
274         self.model.clear()
275         for i in self.balls:
276             self.model.append([i.position, i.name])
277
278     def load_balls_from_file(self, fname):
279         balls = BallManager()
280         if not os.path.exists(fname):
281             return balls
282
283         for index, line in enumerate(file(fname)):
284             if not line:
285                 continue
286             pos, radius, name = line.split(None, 2)
287             x, y = pos.split(',')
288             balls.append(Ball(x, y, radius, name.strip(), index))
289         return balls
290
291     def remove_fp(self, *args):
292         selection = self.treeview.get_selection()
293         model, path = selection.get_selected()
294         if path:
295             position = model[path][0]
296             for i in self.balls:
297                 if i.position == int(position):
298                     self.balls.remove(i)
299             del model[path]
300             self.draw.queue_draw()
301
302     def select_fp(self, treeview, event):
303         path, column, x, y = \
304             self.treeview.get_path_at_pos(int(event.x), int(event.y))
305         if path:
306             model = self.treeview.get_model()
307             ball = self.balls[model[path][0]]
308
309             # making sure that only one ball is selected
310             for i in self.balls:
311                 i.selected = False
312             ball.selected = True
313
314             # available space to the image
315             w = self.evtbox.get_allocation().width
316             h = self.evtbox.get_allocation().height
317
318             # point begining from the left image border
319             wib = self.point_with_border(ball)
320
321             #self.wid('viewport').get_vadjustment().value = wib.x # + (w / 2)
322             #self.wid('viewport').get_hadjustment().value = wib.y # + (h / 2)
323
324             self.draw.queue_draw()
325
326     def select_fp_from_image(self, ball):
327         selection = self.treeview.get_selection()
328         selection.select_path(str(ball.position))
329
330         # making sure that only one ball is selected
331         for i in self.balls:
332             i.selected = False
333         ball.selected = True
334
335         self.draw.queue_draw()
336
337     def save_fp_list(self, *args):
338         assert self.project is not None
339
340         # if the project has no
341         if self.project and not self.project.focus_points_file:
342             fc = gtk.FileChooserDialog(_('Save the focus points file'),
343                                        self.window,
344                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
345                                        buttons=(gtk.STOCK_CANCEL,
346                                                 gtk.RESPONSE_CANCEL,
347                                                 gtk.STOCK_SAVE,
348                                                 gtk.RESPONSE_OK))
349             if fc.run() == gtk.RESPONSE_OK:
350                 self.project.focus_points_file = fc.get_filename()
351                 fc.destroy()
352             else:
353                 fc.destroy()
354                 return
355
356         self.balls.save_to_file(self.project.focus_points_file)
357
358     def move_fp_up(self, *args):
359         selection = self.treeview.get_selection()
360         model, path = selection.get_selected()
361         if not path:
362             return
363
364         pos = model[path][0]
365         newpos = max(pos - 1, 0)
366         self.balls.insert(newpos, self.balls.pop(pos))
367
368         # normalizing the position of elements.
369         for index, item in enumerate(self.balls):
370             item.position = index
371
372         self.load_balls_to_treeview()
373         selection.select_path(str(newpos))
374
375     def move_fp_down(self, *args):
376         selection = self.treeview.get_selection()
377         model, path = selection.get_selected()
378         if not path:
379             return
380
381         pos = model[path][0]
382         newpos = min(pos + 1, len(self.balls))
383         self.balls.insert(newpos, self.balls.pop(pos))
384
385         # normalizing the position of elements.
386         for index, item in enumerate(self.balls):
387             item.position = index
388
389         self.load_balls_to_treeview()
390         selection.select_path(str(newpos))
391
392     def expose_draw(self, draw, event):
393         if not self.image:
394             return
395
396         for i in self.balls:
397             self.draw_ball(i)
398
399         if self.start_x < 0:
400             return False
401
402         if self.new_ball:
403             ball = Ball(self.start_x, self.start_y, self.radius)
404             self.draw_ball(ball)
405
406         return False
407
408     def point_with_border(self, ball):
409         iw, ih = self.draw.size_request()
410         w = self.draw.get_allocation().width
411         h = self.draw.get_allocation().height
412
413         x = ((w / 2) - (iw / 2)) + ball.p.x
414         y = ((h / 2) - (ih / 2)) + ball.p.y
415         return Point(x, y)
416
417     def point_without_border(self, point):
418         iw, ih = self.draw.size_request()
419         w = self.draw.get_allocation().width
420         h = self.draw.get_allocation().height
421
422         x = point.x - ((w / 2) - (iw / 2))
423         y = point.y - ((h / 2) - (ih / 2))
424         return Point(x, y)
425
426     def draw_ball(self, ball):
427         ctx = self.draw.window.cairo_create()
428         ctx.arc(self.point_with_border(ball).x,
429                 self.point_with_border(ball).y,
430                 ball.radius, 0, 64*math.pi)
431         ctx.set_source_rgba(0.0, 0.0, 0.5, 0.4)
432         ctx.fill()
433
434         if ball.selected:
435             ctx.set_source_rgba(0.0, 0.5, 0.0, 0.4)
436             ctx.set_line_width(5)
437             ctx.arc(self.point_with_border(ball).x,
438                     self.point_with_border(ball).y,
439                     ball.radius+1, 0, 64*math.pi)
440             ctx.stroke()
441
442     def button_press(self, widget, event):
443         self.new_ball = True
444
445         self.last_x = event.x
446         self.last_y = event.y
447
448         if event.button == 1:
449             for i in self.balls:
450                 p1 = Point(event.x, event.y)
451                 p2 = self.point_with_border(i)
452                 if Point.pythagorean(p1, p2) < i.radius:
453                     self.last_x = event.x - i.p.x
454                     self.last_y = event.y - i.p.y
455                     self.select_fp_from_image(i)
456
457                     self.new_ball = False
458                     self.move_ball = i
459                     break
460
461             self.start_x = self.point_without_border(event).x
462             self.start_y = self.point_without_border(event).y
463
464     def button_release(self, widget, event):
465         self.move_ball = None
466
467         if event.button == 1:
468             self.finish_drawing()
469
470     def motion_notify(self, widget, event):
471         if not self.new_ball:
472             return
473
474         self.draw.queue_draw()
475
476         if event.x > self.last_x:
477             self.radius += 3
478         else:
479             self.radius -= 3
480
481         self.last_x = event.x
482
483     def ball_motion(self, widget, event):
484         if not self.move_ball:
485             return
486
487         self.move_ball.p.x = self.point_without_border(event).x
488         self.move_ball.p.y = self.point_without_border(event).y
489
490         self.draw.queue_draw()
491
492     def finish_drawing(self):
493         if self.new_ball:
494             position = len(self.balls)
495             ball = Ball(self.start_x, self.start_y, self.radius, '', position)
496             self.balls.append(ball)
497             self.model.append([position, ''])
498             self.treeview.set_cursor(str(position), self.fpcolumn, True)
499             self.new_ball = False
500
501         # reseting to the default coordenades
502         self.start_x = -1
503         self.start_y = -1
504         self.radius = Ball.DEFAULT_WIDTH
505
506 if __name__ == '__main__':
507     Gzv().show()
508     gtk.main()