removing an unuseful attribute (Gzv.selecting)
[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
270     def save_fp_list(self, *args):
271         assert self.project is not None
272
273         # if the project has no
274         if self.project and not self.project.focus_points_file:
275             fc = gtk.FileChooserDialog(_('Save the focus points file'),
276                                        self.window,
277                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
278                                        buttons=(gtk.STOCK_CANCEL,
279                                                 gtk.RESPONSE_CANCEL,
280                                                 gtk.STOCK_SAVE,
281                                                 gtk.RESPONSE_OK))
282             if fc.run() == gtk.RESPONSE_OK:
283                 self.project.focus_points_file = fc.get_filename()
284                 fc.destroy()
285             else:
286                 fc.destroy()
287                 return
288
289         self.balls.save_to_file(self.project.focus_points_file)
290
291     def expose_draw(self, draw, event):
292         if not self.image:
293             return
294
295         self.draw_current_ball()
296         for i in self.balls:
297             self.draw_ball(i)
298         return False
299
300     def draw_ball(self, ball):
301         ctx = self.draw.window.cairo_create()
302         ctx.arc(ball.x, ball.y, ball.radius, 0, 64*math.pi)
303         ctx.set_source_rgba(0.5, 0.0, 0.0, 0.4)
304         ctx.fill()
305
306     def draw_current_ball(self):
307         if self.start_x < 0:
308             return
309         ball = Ball(self.start_x, self.start_y, self.radius)
310         self.draw_ball(ball)
311
312     def button_press(self, widget, event):
313         if event.button == 1:
314             self.start_x = event.x
315             self.start_y = event.y
316             self.last_x = event.x
317
318     def button_release(self, widget, event):
319         if event.button == 1:
320             self.finish_drawing()
321
322     def motion_notify(self, widget, event):
323         self.draw.queue_draw()
324
325         if event.x > self.last_x:
326             self.radius += 3
327         else:
328             self.radius -= 3
329
330         self.last_x = event.x
331
332     def finish_drawing(self):
333         position = len(self.balls)
334         ball = Ball(self.start_x, self.start_y, self.radius, '', position)
335         self.balls.append(ball)
336         self.model.append([position, ''])
337         self.treeview.set_cursor(str(position), self.fpcolumn, True)
338
339         # returning to the standard radius
340         self.radius = Ball.DEFAULT_WIDTH
341
342 if __name__ == '__main__':
343     Gzv().window.show_all()
344     gtk.main()