081e06a181527f2dd877884ffa52be3a067635db
[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):
29         self.position = position
30         self.x = int(x)
31         self.y = int(y)
32         self.radius = int(r)
33         self.name = name
34
35 class BallManager(list):
36     def __init__(self, *args, **kwargs):
37         super(BallManager, self).__init__(*args, **kwargs)
38
39     def save_to_file(self, path):
40         target = open(path, 'w')
41         for i in self:
42             target.write('%d,%d %d %s\n' % (i.x, i.y, i.radius, i.name))
43         target.close()
44
45 class GladeLoader(object):
46     def __init__(self, fname, root=''):
47         self.ui = gtk.glade.XML(fname, root)
48         self.ui.signal_autoconnect(self)
49
50     def get_widget(self, wname):
51         return self.ui.get_widget(wname)
52
53     # little shortcut
54     wid = get_widget
55
56     # glade callbacks
57
58     def gtk_widget_show(self, widget, *args):
59         widget.show()
60         return True
61
62     def gtk_widget_hide(self, widget, *args):
63         widget.hide()
64         return True
65
66     def gtk_main_quit(self, *args):
67         gtk.main_quit()
68
69     def gtk_main(self, *args):
70         gtk.main()
71
72 class Project(object):
73     def __init__(self, image, width, height):
74         self.image = image
75         self.width = width
76         self.height = height
77         self.focus_points_file = ''
78
79     def save_to_file(self, path):
80         if not self.focus_points_file:
81             bn = os.path.basename(path)
82             name = os.path.splitext(bn)[0]
83             self.focus_points_file = \
84                 os.path.join(os.path.dirname(path), name + '_fpf')
85
86         cp = ConfigParser()
87         cp.add_section('Project')
88         cp.set('Project', 'image', self.image)
89         cp.set('Project', 'width', self.width)
90         cp.set('Project', 'height', self.height)
91         cp.set('Project', 'focus_points', self.focus_points_file)
92         
93         cp.write(open(path, 'w'))
94
95     @staticmethod
96     def parse_file(path):
97         cp = ConfigParser()
98         cp.read(path)
99
100         image = cp.get('Project', 'image')
101         width = cp.getint('Project', 'width')
102         height = cp.getint('Project', 'height')
103         x = cp.getint('Project', 'height')
104
105         proj = Project(image, width, height)
106         proj.focus_points_file = cp.get('Project', 'focus_points')
107
108         return proj
109
110 class NewProject(GladeLoader):
111     def __init__(self, parent=None):
112         super(NewProject, self).__init__('gzv.glade', 'new-project')
113         self.dialog = self.wid('new-project')
114         if parent:
115             self.dialog.set_transient_for(parent)
116
117     def get_project(self):
118         fname = self.wid('image').get_filename()
119         width = self.wid('width').get_text()
120         height = self.wid('height').get_text()
121         return Project(fname, width, height)
122
123     def destroy(self):
124         self.dialog.destroy()
125
126 class Gzv(GladeLoader):
127     def __init__(self):
128         super(Gzv, self).__init__('gzv.glade', 'main-window')
129         self.window = self.wid('main-window')
130         self.window.connect('delete-event', lambda *x: gtk.main_quit())
131
132         self.evtbox = self.wid('eventbox')
133         self.evtbox.connect('button-press-event', self.button_press)
134         self.evtbox.connect('button-release-event', self.button_release)
135         self.evtbox.connect('motion-notify-event', self.motion_notify)
136
137         self.model = gtk.ListStore(int, str)
138         self.treeview = self.wid('treeview')
139         self.treeview.set_model(self.model)
140
141         self.draw = self.wid('draw')
142         self.draw.connect('expose-event', self.expose_draw)
143
144         # Starting with an empty project with no image loaded
145         self.project = None
146         self.image = None
147
148         # This attr may be overriten, if so, call the method (load_balls_to_treeview)
149         self.balls = BallManager()
150
151         self.load_balls_to_treeview()
152         self.setup_treeview()
153
154         # drawing stuff
155         self.selecting = False
156         self.start_x = -1
157         self.start_y = -1
158         self.radius = Ball.DEFAULT_WIDTH
159
160     def setup_treeview(self):
161         self.model.connect('rows-reordered', self.on_rows_reordered)
162
163         renderer = gtk.CellRendererText()
164         column = gtk.TreeViewColumn(_('Position'), renderer, text=0)
165         column.set_property('visible', False)
166         self.treeview.append_column(column)
167
168         renderer = gtk.CellRendererText()
169         renderer.connect('edited', self.on_cell_edited)
170         renderer.set_property('editable', True)
171         self.fpcolumn = gtk.TreeViewColumn(_('Name'), renderer, text=1)
172         self.treeview.append_column(self.fpcolumn)
173
174     def on_rows_reordered(self, *args):
175         print 
176
177     def on_cell_edited(self, renderer, path, value):
178         self.balls[int(path)].name = value
179         self.load_balls_to_treeview()
180         self.draw.queue_draw()
181
182     def new_project(self, button):
183         proj = NewProject(self.window)
184
185         # This '1' was defined in the glade file
186         if proj.dialog.run() == 1:
187             self.load_project(proj.get_project())
188         proj.destroy()
189
190     def open_project(self, *args):
191         fc = gtk.FileChooserDialog(_('Choose a gzv project'), self.window,
192                                    buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
193                                             gtk.STOCK_OK, gtk.RESPONSE_OK))
194         if fc.run() == gtk.RESPONSE_OK:
195             proj_file = fc.get_filename()
196             self.load_project(Project.parse_file(proj_file))
197         fc.destroy()
198
199     def save_project(self, *args):
200         fc = gtk.FileChooserDialog(_('Save project'), self.window,
201                                    action=gtk.FILE_CHOOSER_ACTION_SAVE,
202                                    buttons=(gtk.STOCK_CANCEL,
203                                             gtk.RESPONSE_CANCEL,
204                                             gtk.STOCK_SAVE,
205                                             gtk.RESPONSE_OK))
206         if fc.run() == gtk.RESPONSE_OK:
207             self.project.save_to_file(fc.get_filename())
208             self.balls.save_to_file(self.project.focus_points_file)
209         fc.destroy()
210
211
212     def load_project(self, project):
213         self.project = project
214         self.balls = self.load_balls_from_file(project.focus_points_file)
215         self.image = project.image
216         self.load_balls_to_treeview()
217         self.draw.queue_draw()
218
219     def unload_project(self):
220         self.project = None
221         self.image = None
222         self.balls = BallManager()
223         self.draw.queue_draw()
224
225     def load_balls_to_treeview(self):
226         self.model.clear()
227         for i in self.balls:
228             self.model.append([i.position, i.name])
229
230     def load_balls_from_file(self, fname):
231         balls = BallManager()
232         if not os.path.exists(fname):
233             return balls
234
235         for index, line in enumerate(file(fname)):
236             if not line:
237                 continue
238             pos, radius, name = line.split(None, 2)
239             x, y = pos.split(',')
240             balls.append(Ball(x, y, radius, name.strip(), index))
241         return balls
242
243     def remove_fp(self, *args):
244         selection = self.treeview.get_selection()
245         model, path = selection.get_selected()
246         if path:
247             position = model[path][0]
248             for i in self.balls:
249                 if i.position == int(position):
250                     self.balls.remove(i)
251             del model[path]
252             self.draw.queue_draw()
253
254     def save_fp_list(self, *args):
255         assert self.project is not None
256
257         # if the project has no
258         if self.project and not self.project.focus_points_file:
259             fc = gtk.FileChooserDialog(_('Save the focus points file'),
260                                        self.window,
261                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
262                                        buttons=(gtk.STOCK_CANCEL,
263                                                 gtk.RESPONSE_CANCEL,
264                                                 gtk.STOCK_SAVE,
265                                                 gtk.RESPONSE_OK))
266             if fc.run() == gtk.RESPONSE_OK:
267                 self.project.focus_points_file = fc.get_filename()
268                 fc.destroy()
269             else:
270                 fc.destroy()
271                 return
272
273         self.balls.save_to_file(self.project.focus_points_file)
274
275     def expose_draw(self, draw, event):
276         if not self.image:
277             return
278
279         # loading the picture image and getting some useful
280         # information to draw it in the widget's background
281         try:
282             img = gtk.gdk.pixbuf_new_from_file(self.image)
283         except gobject.GError:
284             msg = _("Couldn't recognize the image file format.")
285             dialog = gtk.MessageDialog(self.window,
286                                        gtk.DIALOG_MODAL,
287                                        gtk.MESSAGE_ERROR,
288                                        gtk.BUTTONS_CLOSE)
289             dialog.set_markup(msg)
290             dialog.run()
291             dialog.destroy()
292
293             self.draw.stop_emission('expose-event')
294             return self.unload_project()
295
296         pixels = img.get_pixels()
297         rowstride = img.get_rowstride()
298         width = img.get_width()
299         height = img.get_height()
300         gc = draw.style.black_gc
301
302         # sets the correct size of the eventbox, to show the scrollbar
303         # when needed.
304         self.evtbox.set_size_request(width, height)
305
306         # drawing the picture in the background of the drawing area,
307         # this is really important.
308         draw.window.draw_rgb_image(gc, 0, 0, width, height,
309                                    'normal', pixels, rowstride,
310                                    0, 0)
311
312         # this call makes the ball being drown be shown correctly.
313         self.draw_current_ball()
314
315         # drawing other balls stored in the self.balls list.
316         ctx = draw.window.cairo_create()
317         ctx.fill()
318
319         ctx.set_line_width(10.0)
320         ctx.set_source_rgba(0.5, 0.0, 0.0, 0.4)
321
322         for i in self.balls:
323             ctx.arc(i.x, i.y, i.radius, 0, 64*math.pi)
324             ctx.fill()
325
326     def draw_current_ball(self):
327         if self.start_x < 0:
328             return
329         ctx = self.draw.window.cairo_create()
330         ctx.arc(self.start_x, self.start_y, self.radius, 0, 64*math.pi)
331         ctx.set_source_rgba(0.5, 0.0, 0.0, 0.4)
332         ctx.fill()
333
334     def button_press(self, widget, event):
335         if event.button == 1:
336             self.selecting = True
337             self.start_x = event.x
338             self.start_y = event.y
339             self.last_x = event.x
340
341     def button_release(self, widget, event):
342         if event.button == 1:
343             self.selecting = False
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         self.draw_current_ball()
358         self.ball_width = Ball.DEFAULT_WIDTH
359
360         position = len(self.balls)
361         ball = Ball(self.start_x, self.start_y, self.radius, '', position)
362         self.balls.append(ball)
363         self.model.append([position, ''])
364         self.treeview.set_cursor(str(position), self.fpcolumn, True)
365
366 if __name__ == '__main__':
367     Gzv().window.show_all()
368     gtk.main()