0bccbffcd55b24f6d59c2051bb87491f72b0e6ab
[cascardo/movie.git] / gzv.py
1 # gzv.py - an user interface to generate-zooming-video
2 #
3 # Copyright (C) 2008  Lincoln de Sousa <lincoln@minaslivre.org>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15 import os
16 import gtk
17 import gtk.glade
18 import gobject
19 import math
20 import cairo
21 from ConfigParser import ConfigParser
22
23 _ = lambda x:x
24
25 class Ball(object):
26     DEFAULT_WIDTH = 10
27
28     def __init__(self, x, y, r, name='', position=0, selected=False):
29         self.position = position
30         self.selected = selected
31         self.x = int(x)
32         self.y = int(y)
33         self.radius = int(r)
34         self.name = name
35
36 class BallManager(list):
37     def __init__(self, *args, **kwargs):
38         super(BallManager, self).__init__(*args, **kwargs)
39
40     def save_to_file(self, path):
41         target = open(path, 'w')
42         for i in self:
43             target.write('%d,%d %d %s\n' % (i.x, i.y, i.radius, i.name))
44         target.close()
45
46 class GladeLoader(object):
47     def __init__(self, fname, root=''):
48         self.ui = gtk.glade.XML(fname, root)
49         self.ui.signal_autoconnect(self)
50
51     def get_widget(self, wname):
52         return self.ui.get_widget(wname)
53
54     # little shortcut
55     wid = get_widget
56
57     # glade callbacks
58
59     def gtk_widget_show(self, widget, *args):
60         widget.show()
61         return True
62
63     def gtk_widget_hide(self, widget, *args):
64         widget.hide()
65         return True
66
67     def gtk_main_quit(self, *args):
68         gtk.main_quit()
69
70     def gtk_main(self, *args):
71         gtk.main()
72
73 class Project(object):
74     def __init__(self, image, width, height):
75         self.image = image
76         self.width = width
77         self.height = height
78         self.focus_points_file = ''
79
80     def save_to_file(self, path):
81         if not self.focus_points_file:
82             bn = os.path.basename(path)
83             name = os.path.splitext(bn)[0]
84             self.focus_points_file = \
85                 os.path.join(os.path.dirname(path), name + '_fpf')
86
87         cp = ConfigParser()
88         cp.add_section('Project')
89         cp.set('Project', 'image', self.image)
90         cp.set('Project', 'width', self.width)
91         cp.set('Project', 'height', self.height)
92         cp.set('Project', 'focus_points', self.focus_points_file)
93         
94         cp.write(open(path, 'w'))
95
96     @staticmethod
97     def parse_file(path):
98         cp = ConfigParser()
99         cp.read(path)
100
101         image = cp.get('Project', 'image')
102         width = cp.getint('Project', 'width')
103         height = cp.getint('Project', 'height')
104         x = cp.getint('Project', 'height')
105
106         proj = Project(image, width, height)
107         proj.focus_points_file = cp.get('Project', 'focus_points')
108
109         return proj
110
111 class NewProject(GladeLoader):
112     def __init__(self, parent=None):
113         super(NewProject, self).__init__('gzv.glade', 'new-project')
114         self.dialog = self.wid('new-project')
115         if parent:
116             self.dialog.set_transient_for(parent)
117
118     def get_project(self):
119         if not self.dialog.run():
120             return None
121
122         fname = self.wid('image').get_filename()
123         width = self.wid('width').get_text()
124         height = self.wid('height').get_text()
125         return Project(fname, width, height)
126
127     def destroy(self):
128         self.dialog.destroy()
129
130 class Gzv(GladeLoader):
131     def __init__(self):
132         super(Gzv, self).__init__('gzv.glade', 'main-window')
133         self.window = self.wid('main-window')
134         self.window.connect('delete-event', lambda *x: gtk.main_quit())
135
136         self.evtbox = self.wid('eventbox')
137         self.evtbox.connect('button-press-event', self.button_press)
138         self.evtbox.connect('button-release-event', self.button_release)
139         self.evtbox.connect('motion-notify-event', self.motion_notify)
140
141         self.model = gtk.ListStore(int, str)
142         self.treeview = self.wid('treeview')
143         self.treeview.set_model(self.model)
144         self.treeview.connect('button-press-event', self.select_fp)
145
146         self.draw = self.wid('draw')
147         self.draw.connect_after('expose-event', self.expose_draw)
148
149         # Starting with an empty project with no image loaded
150         self.project = None
151         self.image = None
152
153         # This attr may be overriten, if so, call the method (load_balls_to_treeview)
154         self.balls = BallManager()
155
156         self.load_balls_to_treeview()
157         self.setup_treeview()
158
159         # drawing stuff
160         self.start_x = -1
161         self.start_y = -1
162         self.radius = Ball.DEFAULT_WIDTH
163
164     def setup_treeview(self):
165         self.model.connect('rows-reordered', self.on_rows_reordered)
166
167         renderer = gtk.CellRendererText()
168         column = gtk.TreeViewColumn(_('Position'), renderer, text=0)
169         column.set_property('visible', False)
170         self.treeview.append_column(column)
171
172         renderer = gtk.CellRendererText()
173         renderer.connect('edited', self.on_cell_edited)
174         renderer.set_property('editable', True)
175         self.fpcolumn = gtk.TreeViewColumn(_('Name'), renderer, text=1)
176         self.treeview.append_column(self.fpcolumn)
177
178     def on_rows_reordered(self, *args):
179         print 
180
181     def on_cell_edited(self, renderer, path, value):
182         self.balls[int(path)].name = value
183         self.load_balls_to_treeview()
184
185     def new_project(self, button):
186         proj = NewProject(self.window)
187         project = proj.get_project()
188         proj.destroy()
189
190         # This '1' was defined in the glade file
191         if project:
192             self.load_project(project)
193
194     def open_project(self, *args):
195         fc = gtk.FileChooserDialog(_('Choose a gzv project'), self.window,
196                                    buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
197                                             gtk.STOCK_OK, gtk.RESPONSE_OK))
198         if fc.run() == gtk.RESPONSE_OK:
199             proj_file = fc.get_filename()
200             self.load_project(Project.parse_file(proj_file))
201         fc.destroy()
202
203     def save_project(self, *args):
204         fc = gtk.FileChooserDialog(_('Save project'), self.window,
205                                    action=gtk.FILE_CHOOSER_ACTION_SAVE,
206                                    buttons=(gtk.STOCK_CANCEL,
207                                             gtk.RESPONSE_CANCEL,
208                                             gtk.STOCK_SAVE,
209                                             gtk.RESPONSE_OK))
210         if fc.run() == gtk.RESPONSE_OK:
211             self.project.save_to_file(fc.get_filename())
212             self.balls.save_to_file(self.project.focus_points_file)
213         fc.destroy()
214
215
216     def load_project(self, project):
217         self.project = project
218         self.balls = self.load_balls_from_file(project.focus_points_file)
219         self.image = project.image
220
221         # I'm loading a pixbuf first because I need to get its
222         # dimensions this with a pixbuf is easier than with an image.
223         try:
224             pixbuf = gtk.gdk.pixbuf_new_from_file(project.image)
225         except gobject.GError:
226             msg = _("Couldn't recognize the image file format.")
227             dialog = gtk.MessageDialog(self.window,
228                                        gtk.DIALOG_MODAL,
229                                        gtk.MESSAGE_ERROR,
230                                        gtk.BUTTONS_CLOSE)
231             dialog.set_markup(msg)
232             dialog.run()
233             dialog.destroy()
234             return self.unload_project()
235
236         self.draw.set_from_pixbuf(pixbuf)
237         self.load_balls_to_treeview()
238
239     def unload_project(self):
240         self.project = None
241         self.image = None
242         self.balls = BallManager()
243         self.draw.queue_draw()
244
245     def load_balls_to_treeview(self):
246         self.model.clear()
247         for i in self.balls:
248             self.model.append([i.position, i.name])
249
250     def load_balls_from_file(self, fname):
251         balls = BallManager()
252         if not os.path.exists(fname):
253             return balls
254
255         for index, line in enumerate(file(fname)):
256             if not line:
257                 continue
258             pos, radius, name = line.split(None, 2)
259             x, y = pos.split(',')
260             balls.append(Ball(x, y, radius, name.strip(), index))
261         return balls
262
263     def remove_fp(self, *args):
264         selection = self.treeview.get_selection()
265         model, path = selection.get_selected()
266         if path:
267             position = model[path][0]
268             for i in self.balls:
269                 if i.position == int(position):
270                     self.balls.remove(i)
271             del model[path]
272             self.draw.queue_draw()
273
274     def select_fp(self, treeview, event):
275         path, column, x, y = \
276             self.treeview.get_path_at_pos(int(event.x), int(event.y))
277         if path:
278             model = self.treeview.get_model()
279             ball = self.balls[model[path][0]]
280
281             # making sure that only one ball is selected
282             for i in self.balls:
283                 i.selected = False
284             ball.selected = True
285
286             self.draw.queue_draw()
287
288     def save_fp_list(self, *args):
289         assert self.project is not None
290
291         # if the project has no
292         if self.project and not self.project.focus_points_file:
293             fc = gtk.FileChooserDialog(_('Save the focus points file'),
294                                        self.window,
295                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
296                                        buttons=(gtk.STOCK_CANCEL,
297                                                 gtk.RESPONSE_CANCEL,
298                                                 gtk.STOCK_SAVE,
299                                                 gtk.RESPONSE_OK))
300             if fc.run() == gtk.RESPONSE_OK:
301                 self.project.focus_points_file = fc.get_filename()
302                 fc.destroy()
303             else:
304                 fc.destroy()
305                 return
306
307         self.balls.save_to_file(self.project.focus_points_file)
308
309     def expose_draw(self, draw, event):
310         if not self.image:
311             return
312
313         self.draw_current_ball()
314         for i in self.balls:
315             self.draw_ball(i)
316         return False
317
318     def draw_ball(self, ball):
319         ctx = self.draw.window.cairo_create()
320         ctx.arc(ball.x, ball.y, ball.radius, 0, 64*math.pi)
321         ctx.set_source_rgba(0.0, 0.0, 0.5, 0.4)
322         ctx.fill()
323
324         if ball.selected:
325             ctx.set_source_rgba(0.0, 0.5, 0.0, 0.4)
326             ctx.set_line_width(5)
327             ctx.arc(ball.x, ball.y, ball.radius+1, 0, 64*math.pi)
328             ctx.stroke()
329
330     def draw_current_ball(self):
331         if self.start_x < 0:
332             return
333         ball = Ball(self.start_x, self.start_y, self.radius)
334         self.draw_ball(ball)
335
336     def button_press(self, widget, event):
337         if event.button == 1:
338             self.start_x = event.x
339             self.start_y = event.y
340             self.last_x = event.x
341
342     def button_release(self, widget, event):
343         if event.button == 1:
344             self.finish_drawing()
345
346     def motion_notify(self, widget, event):
347         self.draw.queue_draw()
348
349         if event.x > self.last_x:
350             self.radius += 3
351         else:
352             self.radius -= 3
353
354         self.last_x = event.x
355
356     def finish_drawing(self):
357         position = len(self.balls)
358         ball = Ball(self.start_x, self.start_y, self.radius, '', position)
359         self.balls.append(ball)
360         self.model.append([position, ''])
361         self.treeview.set_cursor(str(position), self.fpcolumn, True)
362
363         # reseting to the default coordenades
364         self.start_x = -1
365         self.start_y = -1
366         self.radius = Ball.DEFAULT_WIDTH
367
368 if __name__ == '__main__':
369     Gzv().window.show_all()
370     gtk.main()