42dce80e0554c489bba7b4221cd01fead3ce3c5e
[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         # This '1' was defined in the glade file
120         if not self.dialog.run() == 1:
121             return None
122
123         fname = self.wid('image').get_filename()
124         width = self.wid('width').get_text()
125         height = self.wid('height').get_text()
126         return Project(fname, width, height)
127
128     def destroy(self):
129         self.dialog.destroy()
130
131 class Gzv(GladeLoader):
132     def __init__(self):
133         super(Gzv, self).__init__('gzv.glade', 'main-window')
134         self.window = self.wid('main-window')
135         self.window.connect('delete-event', lambda *x: gtk.main_quit())
136
137         self.evtbox = self.wid('eventbox')
138         self.evtbox.connect('button-press-event', self.button_press)
139         self.evtbox.connect('button-release-event', self.button_release)
140         self.evtbox.connect('motion-notify-event', self.motion_notify)
141
142         self.model = gtk.ListStore(int, str)
143         self.treeview = self.wid('treeview')
144         self.treeview.set_model(self.model)
145         self.treeview.connect('button-press-event', self.select_fp)
146
147         self.draw = self.wid('draw')
148         self.draw.connect_after('expose-event', self.expose_draw)
149
150         # Starting with an empty project with no image loaded
151         self.project = None
152         self.image = None
153
154         # This attr may be overriten, if so, call the method (load_balls_to_treeview)
155         self.balls = BallManager()
156
157         self.load_balls_to_treeview()
158         self.setup_treeview()
159
160         # drawing stuff
161         self.start_x = -1
162         self.start_y = -1
163         self.radius = Ball.DEFAULT_WIDTH
164
165     def setup_treeview(self):
166         self.model.connect('rows-reordered', self.on_rows_reordered)
167
168         renderer = gtk.CellRendererText()
169         column = gtk.TreeViewColumn(_('Position'), renderer, text=0)
170         column.set_property('visible', False)
171         self.treeview.append_column(column)
172
173         renderer = gtk.CellRendererText()
174         renderer.connect('edited', self.on_cell_edited)
175         renderer.set_property('editable', True)
176         self.fpcolumn = gtk.TreeViewColumn(_('Name'), renderer, text=1)
177         self.treeview.append_column(self.fpcolumn)
178
179     def on_rows_reordered(self, *args):
180         print 
181
182     def on_cell_edited(self, renderer, path, value):
183         self.balls[int(path)].name = value
184         self.load_balls_to_treeview()
185
186     def new_project(self, button):
187         proj = NewProject(self.window)
188         project = proj.get_project()
189         proj.destroy()
190
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         for i in self.balls:
314             self.draw_ball(i)
315
316         if self.start_x < 0:
317             return False
318
319         ball = Ball(self.start_x, self.start_y, self.radius)
320         self.draw_ball(ball)
321
322         return False
323
324     def draw_ball(self, ball):
325         ctx = self.draw.window.cairo_create()
326         ctx.arc(ball.x, ball.y, ball.radius, 0, 64*math.pi)
327         ctx.set_source_rgba(0.0, 0.0, 0.5, 0.4)
328         ctx.fill()
329
330         if ball.selected:
331             ctx.set_source_rgba(0.0, 0.5, 0.0, 0.4)
332             ctx.set_line_width(5)
333             ctx.arc(ball.x, ball.y, ball.radius+1, 0, 64*math.pi)
334             ctx.stroke()
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()