8eb7fdb9bdadc931455d53360e4a2c65498c69ee
[cascardo/movie.git] / gzv.py
1 # -*- coding: utf-8; -*-
2 # gzv.py - an user interface to generate-zooming-video
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', 'hbox2':
270             self.wid(i).set_sensitive(sensitive)
271
272     def load_balls_to_treeview(self):
273         self.model.clear()
274         for i in self.balls:
275             self.model.append([i.position, i.name])
276
277     def load_balls_from_file(self, fname):
278         balls = BallManager()
279         if not os.path.exists(fname):
280             return balls
281
282         for index, line in enumerate(file(fname)):
283             if not line:
284                 continue
285             pos, radius, name = line.split(None, 2)
286             x, y = pos.split(',')
287             balls.append(Ball(x, y, radius, name.strip(), index))
288         return balls
289
290     def remove_fp(self, *args):
291         selection = self.treeview.get_selection()
292         model, path = selection.get_selected()
293         if path:
294             position = model[path][0]
295             for i in self.balls:
296                 if i.position == int(position):
297                     self.balls.remove(i)
298             del model[path]
299             self.draw.queue_draw()
300
301     def select_fp(self, treeview, event):
302         path, column, x, y = \
303             self.treeview.get_path_at_pos(int(event.x), int(event.y))
304         if path:
305             model = self.treeview.get_model()
306             ball = self.balls[model[path][0]]
307
308             # making sure that only one ball is selected
309             for i in self.balls:
310                 i.selected = False
311             ball.selected = True
312
313             # available space to the image
314             w = self.evtbox.get_allocation().width
315             h = self.evtbox.get_allocation().height
316
317             # point begining from the left image border
318             wib = self.point_with_border(ball)
319
320             print wib.x, w
321
322             self.wid('viewport').get_vadjustment().value = wib.x # + (w / 2)
323             self.wid('viewport').get_hadjustment().value = wib.y # + (h / 2)
324
325             self.draw.queue_draw()
326
327     def select_fp_from_image(self, ball):
328         selection = self.treeview.get_selection()
329         selection.select_path(str(ball.position))
330
331         # making sure that only one ball is selected
332         for i in self.balls:
333             i.selected = False
334         ball.selected = True
335
336         self.draw.queue_draw()
337
338     def save_fp_list(self, *args):
339         assert self.project is not None
340
341         # if the project has no
342         if self.project and not self.project.focus_points_file:
343             fc = gtk.FileChooserDialog(_('Save the focus points file'),
344                                        self.window,
345                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
346                                        buttons=(gtk.STOCK_CANCEL,
347                                                 gtk.RESPONSE_CANCEL,
348                                                 gtk.STOCK_SAVE,
349                                                 gtk.RESPONSE_OK))
350             if fc.run() == gtk.RESPONSE_OK:
351                 self.project.focus_points_file = fc.get_filename()
352                 fc.destroy()
353             else:
354                 fc.destroy()
355                 return
356
357         self.balls.save_to_file(self.project.focus_points_file)
358
359     def expose_draw(self, draw, event):
360         if not self.image:
361             return
362
363         for i in self.balls:
364             self.draw_ball(i)
365
366         if self.start_x < 0:
367             return False
368
369         if self.new_ball:
370             ball = Ball(self.start_x, self.start_y, self.radius)
371             self.draw_ball(ball)
372
373         return False
374
375     def point_with_border(self, ball):
376         iw, ih = self.draw.size_request()
377         w = self.draw.get_allocation().width
378         h = self.draw.get_allocation().height
379
380         x = ((w / 2) - (iw / 2)) + ball.p.x
381         y = ((h / 2) - (ih / 2)) + ball.p.y
382         return Point(x, y)
383
384     def point_without_border(self, point):
385         iw, ih = self.draw.size_request()
386         w = self.draw.get_allocation().width
387         h = self.draw.get_allocation().height
388
389         x = point.x - ((w / 2) - (iw / 2))
390         y = point.y - ((h / 2) - (ih / 2))
391         return Point(x, y)
392
393     def draw_ball(self, ball):
394         ctx = self.draw.window.cairo_create()
395         ctx.arc(self.point_with_border(ball).x,
396                 self.point_with_border(ball).y,
397                 ball.radius, 0, 64*math.pi)
398         ctx.set_source_rgba(0.0, 0.0, 0.5, 0.4)
399         ctx.fill()
400
401         if ball.selected:
402             ctx.set_source_rgba(0.0, 0.5, 0.0, 0.4)
403             ctx.set_line_width(5)
404             ctx.arc(self.point_with_border(ball).x,
405                     self.point_with_border(ball).y,
406                     ball.radius+1, 0, 64*math.pi)
407             ctx.stroke()
408
409     def button_press(self, widget, event):
410         self.new_ball = True
411
412         self.last_x = event.x
413         self.last_y = event.y
414
415         if event.button == 1:
416             for i in self.balls:
417                 p1 = Point(event.x, event.y)
418                 p2 = self.point_with_border(i)
419                 if Point.pythagorean(p1, p2) < i.radius:
420                     self.last_x = event.x - i.p.x
421                     self.last_y = event.y - i.p.y
422                     self.select_fp_from_image(i)
423
424                     self.new_ball = False
425                     self.move_ball = i
426                     break
427
428             self.start_x = self.point_without_border(event).x
429             self.start_y = self.point_without_border(event).y
430
431     def button_release(self, widget, event):
432         self.move_ball = None
433
434         if event.button == 1:
435             self.finish_drawing()
436
437     def motion_notify(self, widget, event):
438         if not self.new_ball:
439             return
440
441         self.draw.queue_draw()
442
443         if event.x > self.last_x:
444             self.radius += 3
445         else:
446             self.radius -= 3
447
448         self.last_x = event.x
449
450     def ball_motion(self, widget, event):
451         if not self.move_ball:
452             return
453
454         self.move_ball.p.x = self.point_without_border(event).x
455         self.move_ball.p.y = self.point_without_border(event).y
456
457         self.draw.queue_draw()
458
459     def finish_drawing(self):
460         if self.new_ball:
461             position = len(self.balls)
462             ball = Ball(self.start_x, self.start_y, self.radius, '', position)
463             self.balls.append(ball)
464             self.model.append([position, ''])
465             self.treeview.set_cursor(str(position), self.fpcolumn, True)
466             self.new_ball = False
467
468         # reseting to the default coordenades
469         self.start_x = -1
470         self.start_y = -1
471         self.radius = Ball.DEFAULT_WIDTH
472
473 if __name__ == '__main__':
474     Gzv().show()
475     gtk.main()