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