adding the attribute last_x to the Gzv.__init__ method and
[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         # making it possible to grab motion events when the mouse is
143         # over the widget.
144         self.evtbox.set_events(gtk.gdk.POINTER_MOTION_MASK)
145
146         self.model = gtk.ListStore(int, str)
147         self.treeview = self.wid('treeview')
148         self.treeview.set_model(self.model)
149         self.treeview.connect('button-press-event', self.select_fp)
150
151         self.draw = self.wid('draw')
152         self.draw.connect_after('expose-event', self.expose_draw)
153
154         # Starting with an empty project with no image loaded
155         self.project = None
156         self.image = None
157
158         # This attr may be overriten, if so, call the method (load_balls_to_treeview)
159         self.balls = BallManager()
160
161         self.load_balls_to_treeview()
162         self.setup_treeview()
163
164         # drawing stuff
165         self.start_x = -1
166         self.start_y = -1
167         self.last_x = -1
168         self.radius = Ball.DEFAULT_WIDTH
169
170     def show(self):
171         self.window.show_all()
172
173     def setup_treeview(self):
174         self.model.connect('rows-reordered', self.on_rows_reordered)
175
176         renderer = gtk.CellRendererText()
177         column = gtk.TreeViewColumn(_('Position'), renderer, text=0)
178         column.set_property('visible', False)
179         self.treeview.append_column(column)
180
181         renderer = gtk.CellRendererText()
182         renderer.connect('edited', self.on_cell_edited)
183         renderer.set_property('editable', True)
184         self.fpcolumn = gtk.TreeViewColumn(_('Name'), renderer, text=1)
185         self.treeview.append_column(self.fpcolumn)
186
187     def on_rows_reordered(self, *args):
188         print 
189
190     def on_cell_edited(self, renderer, path, value):
191         self.balls[int(path)].name = value
192         self.load_balls_to_treeview()
193
194     def new_project(self, button):
195         proj = NewProject(self.window)
196         project = proj.get_project()
197         proj.destroy()
198
199         if project:
200             self.load_project(project)
201
202     def open_project(self, *args):
203         fc = gtk.FileChooserDialog(_('Choose a gzv project'), self.window,
204                                    buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
205                                             gtk.STOCK_OK, gtk.RESPONSE_OK))
206         if fc.run() == gtk.RESPONSE_OK:
207             proj_file = fc.get_filename()
208             self.load_project(Project.parse_file(proj_file))
209         fc.destroy()
210
211     def save_project(self, *args):
212         fc = gtk.FileChooserDialog(_('Save project'), self.window,
213                                    action=gtk.FILE_CHOOSER_ACTION_SAVE,
214                                    buttons=(gtk.STOCK_CANCEL,
215                                             gtk.RESPONSE_CANCEL,
216                                             gtk.STOCK_SAVE,
217                                             gtk.RESPONSE_OK))
218         if fc.run() == gtk.RESPONSE_OK:
219             self.project.save_to_file(fc.get_filename())
220             self.balls.save_to_file(self.project.focus_points_file)
221         fc.destroy()
222
223
224     def load_project(self, project):
225         self.project = project
226         self.balls = self.load_balls_from_file(project.focus_points_file)
227         self.image = project.image
228
229         # I'm loading a pixbuf first because I need to get its
230         # dimensions this with a pixbuf is easier than with an image.
231         try:
232             pixbuf = gtk.gdk.pixbuf_new_from_file(project.image)
233         except gobject.GError:
234             msg = _("Couldn't recognize the image file format.")
235             dialog = gtk.MessageDialog(self.window,
236                                        gtk.DIALOG_MODAL,
237                                        gtk.MESSAGE_ERROR,
238                                        gtk.BUTTONS_CLOSE)
239             dialog.set_markup(msg)
240             dialog.run()
241             dialog.destroy()
242             return self.unload_project()
243
244         self.draw.set_from_pixbuf(pixbuf)
245         self.load_balls_to_treeview()
246
247     def unload_project(self):
248         self.project = None
249         self.image = None
250         self.balls = BallManager()
251         self.draw.queue_draw()
252
253     def load_balls_to_treeview(self):
254         self.model.clear()
255         for i in self.balls:
256             self.model.append([i.position, i.name])
257
258     def load_balls_from_file(self, fname):
259         balls = BallManager()
260         if not os.path.exists(fname):
261             return balls
262
263         for index, line in enumerate(file(fname)):
264             if not line:
265                 continue
266             pos, radius, name = line.split(None, 2)
267             x, y = pos.split(',')
268             balls.append(Ball(x, y, radius, name.strip(), index))
269         return balls
270
271     def remove_fp(self, *args):
272         selection = self.treeview.get_selection()
273         model, path = selection.get_selected()
274         if path:
275             position = model[path][0]
276             for i in self.balls:
277                 if i.position == int(position):
278                     self.balls.remove(i)
279             del model[path]
280             self.draw.queue_draw()
281
282     def select_fp(self, treeview, event):
283         path, column, x, y = \
284             self.treeview.get_path_at_pos(int(event.x), int(event.y))
285         if path:
286             model = self.treeview.get_model()
287             ball = self.balls[model[path][0]]
288
289             # making sure that only one ball is selected
290             for i in self.balls:
291                 i.selected = False
292             ball.selected = True
293
294             self.draw.queue_draw()
295
296     def save_fp_list(self, *args):
297         assert self.project is not None
298
299         # if the project has no
300         if self.project and not self.project.focus_points_file:
301             fc = gtk.FileChooserDialog(_('Save the focus points file'),
302                                        self.window,
303                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
304                                        buttons=(gtk.STOCK_CANCEL,
305                                                 gtk.RESPONSE_CANCEL,
306                                                 gtk.STOCK_SAVE,
307                                                 gtk.RESPONSE_OK))
308             if fc.run() == gtk.RESPONSE_OK:
309                 self.project.focus_points_file = fc.get_filename()
310                 fc.destroy()
311             else:
312                 fc.destroy()
313                 return
314
315         self.balls.save_to_file(self.project.focus_points_file)
316
317     def expose_draw(self, draw, event):
318         if not self.image:
319             return
320
321         for i in self.balls:
322             self.draw_ball(i)
323
324         if self.start_x < 0:
325             return False
326
327         ball = Ball(self.start_x, self.start_y, self.radius)
328         self.draw_ball(ball)
329
330         return False
331
332     def draw_ball(self, ball):
333         ctx = self.draw.window.cairo_create()
334         ctx.arc(ball.x, ball.y, ball.radius, 0, 64*math.pi)
335         ctx.set_source_rgba(0.0, 0.0, 0.5, 0.4)
336         ctx.fill()
337
338         if ball.selected:
339             ctx.set_source_rgba(0.0, 0.5, 0.0, 0.4)
340             ctx.set_line_width(5)
341             ctx.arc(ball.x, ball.y, ball.radius+1, 0, 64*math.pi)
342             ctx.stroke()
343
344     def button_press(self, widget, event):
345         if event.button == 1:
346             self.start_x = event.x
347             self.start_y = event.y
348             self.last_x = event.x
349
350     def button_release(self, widget, event):
351         if event.button == 1:
352             self.finish_drawing()
353
354     def motion_notify(self, widget, event):
355         self.draw.queue_draw()
356
357         if event.x > self.last_x:
358             self.radius += 3
359         else:
360             self.radius -= 3
361
362         self.last_x = event.x
363
364     def finish_drawing(self):
365         position = len(self.balls)
366         ball = Ball(self.start_x, self.start_y, self.radius, '', position)
367         self.balls.append(ball)
368         self.model.append([position, ''])
369         self.treeview.set_cursor(str(position), self.fpcolumn, True)
370
371         # reseting to the default coordenades
372         self.start_x = -1
373         self.start_y = -1
374         self.radius = Ball.DEFAULT_WIDTH
375
376 if __name__ == '__main__':
377     Gzv().show()
378     gtk.main()