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