Using a gtk image instead of a pixbuf, it is really faster.
[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         if not self.dialog.run():
119             return None
120
121         fname = self.wid('image').get_filename()
122         width = self.wid('width').get_text()
123         height = self.wid('height').get_text()
124         return Project(fname, width, height)
125
126     def destroy(self):
127         self.dialog.destroy()
128
129 class Gzv(GladeLoader):
130     def __init__(self):
131         super(Gzv, self).__init__('gzv.glade', 'main-window')
132         self.window = self.wid('main-window')
133         self.window.connect('delete-event', lambda *x: gtk.main_quit())
134
135         self.evtbox = self.wid('eventbox')
136         self.evtbox.connect('button-press-event', self.button_press)
137         self.evtbox.connect('button-release-event', self.button_release)
138         self.evtbox.connect('motion-notify-event', self.motion_notify)
139
140         self.model = gtk.ListStore(int, str)
141         self.treeview = self.wid('treeview')
142         self.treeview.set_model(self.model)
143
144         self.draw = self.wid('draw')
145         self.draw.connect_after('expose-event', self.expose_draw)
146
147         # Starting with an empty project with no image loaded
148         self.project = None
149         self.image = None
150
151         # This attr may be overriten, if so, call the method (load_balls_to_treeview)
152         self.balls = BallManager()
153
154         self.load_balls_to_treeview()
155         self.setup_treeview()
156
157         # drawing stuff
158         self.selecting = False
159         self.start_x = -1
160         self.start_y = -1
161         self.radius = Ball.DEFAULT_WIDTH
162
163     def setup_treeview(self):
164         self.model.connect('rows-reordered', self.on_rows_reordered)
165
166         renderer = gtk.CellRendererText()
167         column = gtk.TreeViewColumn(_('Position'), renderer, text=0)
168         column.set_property('visible', False)
169         self.treeview.append_column(column)
170
171         renderer = gtk.CellRendererText()
172         renderer.connect('edited', self.on_cell_edited)
173         renderer.set_property('editable', True)
174         self.fpcolumn = gtk.TreeViewColumn(_('Name'), renderer, text=1)
175         self.treeview.append_column(self.fpcolumn)
176
177     def on_rows_reordered(self, *args):
178         print 
179
180     def on_cell_edited(self, renderer, path, value):
181         self.balls[int(path)].name = value
182         self.load_balls_to_treeview()
183
184     def new_project(self, button):
185         proj = NewProject(self.window)
186         project = proj.get_project()
187         proj.destroy()
188
189         # This '1' was defined in the glade file
190         if project:
191             self.load_project(project)
192
193     def open_project(self, *args):
194         fc = gtk.FileChooserDialog(_('Choose a gzv project'), self.window,
195                                    buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
196                                             gtk.STOCK_OK, gtk.RESPONSE_OK))
197         if fc.run() == gtk.RESPONSE_OK:
198             proj_file = fc.get_filename()
199             self.load_project(Project.parse_file(proj_file))
200         fc.destroy()
201
202     def save_project(self, *args):
203         fc = gtk.FileChooserDialog(_('Save project'), self.window,
204                                    action=gtk.FILE_CHOOSER_ACTION_SAVE,
205                                    buttons=(gtk.STOCK_CANCEL,
206                                             gtk.RESPONSE_CANCEL,
207                                             gtk.STOCK_SAVE,
208                                             gtk.RESPONSE_OK))
209         if fc.run() == gtk.RESPONSE_OK:
210             self.project.save_to_file(fc.get_filename())
211             self.balls.save_to_file(self.project.focus_points_file)
212         fc.destroy()
213
214
215     def load_project(self, project):
216         self.project = project
217         self.balls = self.load_balls_from_file(project.focus_points_file)
218         self.image = project.image
219
220         # loading the picture image and getting some useful
221         # information to draw it in the widget's background
222         try:
223             self.draw.set_from_file(project.image)
224         except gobject.GError:
225             msg = _("Couldn't recognize the image file format.")
226             dialog = gtk.MessageDialog(self.window,
227                                        gtk.DIALOG_MODAL,
228                                        gtk.MESSAGE_ERROR,
229                                        gtk.BUTTONS_CLOSE)
230             dialog.set_markup(msg)
231             dialog.run()
232             dialog.destroy()
233             return self.unload_project()
234
235         self.load_balls_to_treeview()
236
237     def unload_project(self):
238         self.project = None
239         self.image = None
240         self.balls = BallManager()
241         self.draw.queue_draw()
242
243     def load_balls_to_treeview(self):
244         self.model.clear()
245         for i in self.balls:
246             self.model.append([i.position, i.name])
247
248     def load_balls_from_file(self, fname):
249         balls = BallManager()
250         if not os.path.exists(fname):
251             return balls
252
253         for index, line in enumerate(file(fname)):
254             if not line:
255                 continue
256             pos, radius, name = line.split(None, 2)
257             x, y = pos.split(',')
258             balls.append(Ball(x, y, radius, name.strip(), index))
259         return balls
260
261     def remove_fp(self, *args):
262         selection = self.treeview.get_selection()
263         model, path = selection.get_selected()
264         if path:
265             position = model[path][0]
266             for i in self.balls:
267                 if i.position == int(position):
268                     self.balls.remove(i)
269             del model[path]
270             self.draw.queue_draw()
271
272     def save_fp_list(self, *args):
273         assert self.project is not None
274
275         # if the project has no
276         if self.project and not self.project.focus_points_file:
277             fc = gtk.FileChooserDialog(_('Save the focus points file'),
278                                        self.window,
279                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
280                                        buttons=(gtk.STOCK_CANCEL,
281                                                 gtk.RESPONSE_CANCEL,
282                                                 gtk.STOCK_SAVE,
283                                                 gtk.RESPONSE_OK))
284             if fc.run() == gtk.RESPONSE_OK:
285                 self.project.focus_points_file = fc.get_filename()
286                 fc.destroy()
287             else:
288                 fc.destroy()
289                 return
290
291         self.balls.save_to_file(self.project.focus_points_file)
292
293     def expose_draw(self, draw, event):
294         if not self.image:
295             return
296
297         self.draw_current_ball()
298         for i in self.balls:
299             self.draw_ball(i)
300         return False
301
302     def draw_ball(self, ball):
303         ctx = self.draw.window.cairo_create()
304         ctx.arc(ball.x, ball.y, ball.radius, 0, 64*math.pi)
305         ctx.set_source_rgba(0.5, 0.0, 0.0, 0.4)
306         ctx.fill()
307
308     def draw_current_ball(self):
309         if self.start_x < 0:
310             return
311         ball = Ball(self.start_x, self.start_y, self.radius)
312         self.draw_ball(ball)
313
314     def button_press(self, widget, event):
315         if event.button == 1:
316             self.selecting = True
317             self.start_x = event.x
318             self.start_y = event.y
319             self.last_x = event.x
320
321     def button_release(self, widget, event):
322         if event.button == 1:
323             self.selecting = False
324             self.finish_drawing()
325
326     def motion_notify(self, widget, event):
327         self.draw.queue_draw()
328
329         if event.x > self.last_x:
330             self.radius += 3
331         else:
332             self.radius -= 3
333
334         self.last_x = event.x
335
336     def finish_drawing(self):
337         position = len(self.balls)
338         ball = Ball(self.start_x, self.start_y, self.radius, '', position)
339         self.balls.append(ball)
340         self.model.append([position, ''])
341         self.treeview.set_cursor(str(position), self.fpcolumn, True)
342
343         # returning to the standard radius
344         self.radius = Ball.DEFAULT_WIDTH
345
346 if __name__ == '__main__':
347     Gzv().window.show_all()
348     gtk.main()