renaming Gzv.ball_with_border to Gzv.point_with_border
[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
238     def load_project(self, project):
239         self.project = project
240         self.balls = self.load_balls_from_file(project.focus_points_file)
241         self.image = project.image
242
243         # I'm loading a pixbuf first because I need to get its
244         # dimensions this with a pixbuf is easier than with an image.
245         try:
246             pixbuf = gtk.gdk.pixbuf_new_from_file(project.image)
247         except gobject.GError:
248             msg = _("Couldn't recognize the image file format.")
249             dialog = gtk.MessageDialog(self.window,
250                                        gtk.DIALOG_MODAL,
251                                        gtk.MESSAGE_ERROR,
252                                        gtk.BUTTONS_CLOSE)
253             dialog.set_markup(msg)
254             dialog.run()
255             dialog.destroy()
256             return self.unload_project()
257
258         self.draw.set_from_pixbuf(pixbuf)
259         self.load_balls_to_treeview()
260
261     def unload_project(self):
262         self.project = None
263         self.image = None
264         self.balls = BallManager()
265         self.draw.queue_draw()
266
267     def load_balls_to_treeview(self):
268         self.model.clear()
269         for i in self.balls:
270             self.model.append([i.position, i.name])
271
272     def load_balls_from_file(self, fname):
273         balls = BallManager()
274         if not os.path.exists(fname):
275             return balls
276
277         for index, line in enumerate(file(fname)):
278             if not line:
279                 continue
280             pos, radius, name = line.split(None, 2)
281             x, y = pos.split(',')
282             balls.append(Ball(x, y, radius, name.strip(), index))
283         return balls
284
285     def remove_fp(self, *args):
286         selection = self.treeview.get_selection()
287         model, path = selection.get_selected()
288         if path:
289             position = model[path][0]
290             for i in self.balls:
291                 if i.position == int(position):
292                     self.balls.remove(i)
293             del model[path]
294             self.draw.queue_draw()
295
296     def select_fp(self, treeview, event):
297         path, column, x, y = \
298             self.treeview.get_path_at_pos(int(event.x), int(event.y))
299         if path:
300             model = self.treeview.get_model()
301             ball = self.balls[model[path][0]]
302
303             # making sure that only one ball is selected
304             for i in self.balls:
305                 i.selected = False
306             ball.selected = True
307
308             self.draw.queue_draw()
309
310     def select_fp_from_image(self, ball):
311         selection = self.treeview.get_selection()
312         selection.select_path(str(ball.position))
313
314         # making sure that only one ball is selected
315         for i in self.balls:
316             i.selected = False
317         ball.selected = True
318
319         self.draw.queue_draw()
320
321     def save_fp_list(self, *args):
322         assert self.project is not None
323
324         # if the project has no
325         if self.project and not self.project.focus_points_file:
326             fc = gtk.FileChooserDialog(_('Save the focus points file'),
327                                        self.window,
328                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
329                                        buttons=(gtk.STOCK_CANCEL,
330                                                 gtk.RESPONSE_CANCEL,
331                                                 gtk.STOCK_SAVE,
332                                                 gtk.RESPONSE_OK))
333             if fc.run() == gtk.RESPONSE_OK:
334                 self.project.focus_points_file = fc.get_filename()
335                 fc.destroy()
336             else:
337                 fc.destroy()
338                 return
339
340         self.balls.save_to_file(self.project.focus_points_file)
341
342     def expose_draw(self, draw, event):
343         if not self.image:
344             return
345
346         for i in self.balls:
347             self.draw_ball(i)
348
349         if self.start_x < 0:
350             return False
351
352         if self.new_ball:
353             ball = Ball(self.start_x, self.start_y, self.radius)
354             self.draw_ball(ball)
355
356         return False
357
358     def point_with_border(self, ball):
359         iw, ih = self.draw.size_request()
360         w = self.draw.get_allocation().width
361         h = self.draw.get_allocation().height
362
363         x = ((w / 2) - (iw / 2)) + ball.p.x
364         y = ((h / 2) - (ih / 2)) + ball.p.y
365         return Point(x, y)
366
367     def point_without_border(self, point):
368         iw, ih = self.draw.size_request()
369         w = self.draw.get_allocation().width
370         h = self.draw.get_allocation().height
371
372         x = point.x - ((w / 2) - (iw / 2))
373         y = point.y - ((h / 2) - (ih / 2))
374         return Point(x, y)
375
376     def draw_ball(self, ball):
377         ctx = self.draw.window.cairo_create()
378         ctx.arc(self.point_with_border(ball).x,
379                 self.point_with_border(ball).y,
380                 ball.radius, 0, 64*math.pi)
381         ctx.set_source_rgba(0.0, 0.0, 0.5, 0.4)
382         ctx.fill()
383
384         if ball.selected:
385             ctx.set_source_rgba(0.0, 0.5, 0.0, 0.4)
386             ctx.set_line_width(5)
387             ctx.arc(self.point_with_border(ball).x,
388                     self.point_with_border(ball).y,
389                     ball.radius+1, 0, 64*math.pi)
390             ctx.stroke()
391
392     def button_press(self, widget, event):
393         self.new_ball = True
394
395         self.last_x = event.x
396         self.last_y = event.y
397
398         if event.button == 1:
399             for i in self.balls:
400                 p1 = Point(event.x, event.y)
401                 p2 = self.point_with_border(i)
402                 if Point.pythagorean(p1, p2) < i.radius:
403                     self.last_x = event.x - i.p.x
404                     self.last_y = event.y - i.p.y
405                     self.select_fp_from_image(i)
406
407                     self.new_ball = False
408                     self.move_ball = i
409                     break
410
411             self.start_x = self.point_without_border(event).x
412             self.start_y = self.point_without_border(event).y
413
414     def button_release(self, widget, event):
415         self.move_ball = None
416
417         if event.button == 1:
418             self.finish_drawing()
419
420     def motion_notify(self, widget, event):
421         if not self.new_ball:
422             return
423
424         self.draw.queue_draw()
425
426         if event.x > self.last_x:
427             self.radius += 3
428         else:
429             self.radius -= 3
430
431         self.last_x = event.x
432
433     def ball_motion(self, widget, event):
434         if not self.move_ball:
435             return
436
437         self.move_ball.p.x = self.point_without_border(event).x
438         self.move_ball.p.y = self.point_without_border(event).y
439
440         self.draw.queue_draw()
441
442     def finish_drawing(self):
443         if self.new_ball:
444             position = len(self.balls)
445             ball = Ball(self.start_x, self.start_y, self.radius, '', position)
446             self.balls.append(ball)
447             self.model.append([position, ''])
448             self.treeview.set_cursor(str(position), self.fpcolumn, True)
449             self.new_ball = False
450
451         # reseting to the default coordenades
452         self.start_x = -1
453         self.start_y = -1
454         self.radius = Ball.DEFAULT_WIDTH
455
456 if __name__ == '__main__':
457     Gzv().show()
458     gtk.main()