~nico-inattendu/luciole/bug_727175

« back to all changes in this revision

Viewing changes to pitivi/application.py

  • Committer: NicoInattendu
  • Date: 2011-02-28 18:27:56 UTC
  • mfrom: (123.1.54 luciole-with-sound)
  • Revision ID: nico@inattendu.org-20110228182756-weonszu8zpzermrl
initial merge with luciole-with-sound branch

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# PiTiVi , Non-linear video editor
 
2
#
 
3
#       pitivi/pitivi.py
 
4
#
 
5
# Copyright (c) 2005-2009 Edward Hervey <bilboed@bilboed.com>
 
6
# Copyright (c) 2008-2009 Alessandro Decina <alessandro.d@gmail.com>
 
7
#
 
8
# This program is free software; you can redistribute it and/or
 
9
# modify it under the terms of the GNU Lesser General Public
 
10
# License as published by the Free Software Foundation; either
 
11
# version 2.1 of the License, or (at your option) any later version.
 
12
#
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
16
# Lesser General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU Lesser General Public
 
19
# License along with this program; if not, write to the
 
20
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 
21
# Boston, MA 02111-1307, USA.
 
22
 
 
23
"""
 
24
Main application
 
25
"""
 
26
import gobject
 
27
gobject.threads_init()
 
28
import gtk
 
29
from optparse import OptionParser
 
30
import os
 
31
import sys
 
32
import urllib
 
33
 
 
34
from pitivi.pitivigstutils import patch_gst_python
 
35
patch_gst_python()
 
36
 
 
37
from gettext import gettext as _
 
38
 
 
39
import pitivi.instance as instance
 
40
 
 
41
from pitivi.check import initial_checks
 
42
from pitivi.device import get_probe
 
43
from pitivi.effects import EffectsHandler
 
44
from pitivi.configure import APPNAME
 
45
from pitivi.settings import GlobalSettings
 
46
from pitivi.threads import ThreadMaster
 
47
from pitivi.pluginmanager import PluginManager
 
48
from pitivi.signalinterface import Signallable
 
49
from pitivi.log.loggable import Loggable
 
50
from pitivi.log import log
 
51
from pitivi.ui.mainwindow import PitiviMainWindow
 
52
from pitivi.projectmanager import ProjectManager, ProjectLogObserver
 
53
from pitivi.undo import UndoableActionLog, DebugActionLogObserver
 
54
from pitivi.timeline.timeline_undo import TimelineLogObserver
 
55
from pitivi.sourcelist_undo import SourceListLogObserver
 
56
from pitivi.undo import UndoableAction
 
57
from pitivi.ui.viewer import PitiviViewer
 
58
from pitivi.actioner import Renderer, Previewer
 
59
 
 
60
# FIXME : Speedup loading time
 
61
# Currently we load everything in one go
 
62
# It would be better if a minimalistic UI could start up ASAP, without loading
 
63
# anything gst-related or that could slow down startup.
 
64
# AND THEN load up the required parts.
 
65
# This will result in a much better end-user experience
 
66
 
 
67
# FIXME : maybe we should have subclasses for UI and CLI
 
68
 
 
69
class Pitivi(Loggable, Signallable):
 
70
    """
 
71
    Pitivi's main application class.
 
72
 
 
73
    Signals:
 
74
     - C{new-project} : A new C{Project} is loaded and ready to use.
 
75
 
 
76
     - C{new-project-loading} : Pitivi is attempting to load a new project.
 
77
     - C{new-project-loaded} : A new L{Project} has been loaded, and the UI should refresh it's view.
 
78
     - C{new-project-failed} : A new L{Project} failed to load.
 
79
     - C{closing-project} :  pitivi would like to close a project. handlers should return false
 
80
     if they do not want this project to close. by default, assumes
 
81
     true. This signal should only be used by classes that might want to abort
 
82
     the closing of a project.
 
83
     - C{project-closed} : The project is closed, it will be freed when the callback returns.
 
84
     Classes should connect to this instance when they want to know that
 
85
     data related to that project is no longer going to be used.
 
86
     - C{shutdown} : Used internally, do not use this signal.`
 
87
 
 
88
    @ivar settings: Application-wide settings.
 
89
    @type settings: L{GlobalSettings}.
 
90
    @ivar projects: List of used projects
 
91
    @type projects: List of L{Project}.
 
92
    @ivar current: Currently used project.
 
93
    @type current: L{Project}.
 
94
    """
 
95
 
 
96
    __signals__ = {
 
97
        "new-project" : ["project"],
 
98
 
 
99
        "new-project-loading" : ["uri"],
 
100
        "new-project-created" : ["project"],
 
101
        "new-project-loaded" : ["project"],
 
102
        "new-project-failed" : ["uri", "exception"],
 
103
        "closing-project" : ["project"],
 
104
        "project-closed" : ["project"],
 
105
        "missing-uri" : ["formatter", "uri"],
 
106
        "shutdown" : None
 
107
        }
 
108
 
 
109
    def __init__(self):
 
110
        """
 
111
        initialize pitivi with the command line arguments
 
112
        """
 
113
        Loggable.__init__(self)
 
114
 
 
115
        # init logging as early as possible so we can log startup code
 
116
        enable_color = os.environ.get('PITIVI_DEBUG_NO_COLOR', '0') in ('', '0')
 
117
        log.init('PITIVI_DEBUG', enable_color)
 
118
 
 
119
        self.info('starting up')
 
120
 
 
121
        # store ourself in the instance global
 
122
        if instance.PiTiVi:
 
123
            raise RuntimeWarning(
 
124
                _("There is already a %s instance, please inform the developers by filing a bug at http://bugzilla.gnome.org/enter_bug.cgi?product=pitivi")
 
125
                % APPNAME)
 
126
        instance.PiTiVi = self
 
127
 
 
128
        self.projects = []
 
129
        self.current = None
 
130
 
 
131
        # get settings
 
132
        self.settings = GlobalSettings()
 
133
        self.threads = ThreadMaster()
 
134
        #self.screencast = False
 
135
 
 
136
        self.plugin_manager = PluginManager(
 
137
            self.settings.get_local_plugin_path(),
 
138
            self.settings.get_plugin_settings_path())
 
139
        self.effects = EffectsHandler()
 
140
        self.deviceprobe = get_probe()
 
141
 
 
142
        self.projectManager = ProjectManager(self.effects)
 
143
        self._connectToProjectManager(self.projectManager)
 
144
 
 
145
        self.action_log = UndoableActionLog()
 
146
        self.debug_action_log_observer = DebugActionLogObserver()
 
147
        self.debug_action_log_observer.startObserving(self.action_log)
 
148
        self.timelineLogObserver = TimelineLogObserver(self.action_log)
 
149
        self.projectLogObserver = ProjectLogObserver(self.action_log)
 
150
        self.sourcelist_log_observer = SourceListLogObserver(self.action_log)
 
151
 
 
152
    #{ Shutdown methods
 
153
 
 
154
    def shutdown(self):
 
155
        """
 
156
        Close PiTiVi.
 
157
 
 
158
        @return: C{True} if PiTiVi was successfully closed, else C{False}.
 
159
        @rtype: C{bool}
 
160
        """
 
161
        self.debug("shutting down")
 
162
        # we refuse to close if we're running a user interface and the user
 
163
        # doesn't want us to close the current project.
 
164
        if self.projectManager.current and not self.projectManager.closeRunningProject():
 
165
            self.warning("Not closing since running project doesn't want to close")
 
166
            return False
 
167
        self.threads.stopAllThreads()
 
168
        self.settings.storeSettings()
 
169
        if self.deviceprobe:
 
170
            self.deviceprobe.release()
 
171
        self.deviceprobe = None
 
172
        self.current = None
 
173
        instance.PiTiVi = None
 
174
        self.emit("shutdown")
 
175
        return True
 
176
 
 
177
    #}
 
178
 
 
179
    def _connectToProjectManager(self, projectManager):
 
180
        projectManager.connect("new-project-loading",
 
181
                self._projectManagerNewProjectLoading)
 
182
        projectManager.connect("new-project-created",
 
183
                self._projectManagerNewProjectCreated)
 
184
        projectManager.connect("new-project-loaded",
 
185
                self._projectManagerNewProjectLoaded)
 
186
        projectManager.connect("new-project-failed",
 
187
                self._projectManagerNewProjectFailed)
 
188
        projectManager.connect("closing-project",
 
189
                self._projectManagerClosingProject)
 
190
        projectManager.connect("project-closed",
 
191
                self._projectManagerProjectClosed)
 
192
 
 
193
    def _projectManagerNewProjectLoading(self, projectManager, uri):
 
194
        self.emit("new-project-loading", uri)
 
195
 
 
196
    def _projectManagerNewProjectCreated(self, projectManager, project):
 
197
        self.current = project
 
198
        self.emit("new-project-created", project)
 
199
 
 
200
    def _newProjectLoaded(self, project):
 
201
        pass
 
202
 
 
203
    def _projectManagerNewProjectLoaded(self, projectManager, project):
 
204
        self.current = project
 
205
        self.action_log.clean()
 
206
        self.timelineLogObserver.startObserving(project.timeline)
 
207
        self.projectLogObserver.startObserving(project)
 
208
        self.sourcelist_log_observer.startObserving(project.sources)
 
209
        self._newProjectLoaded(project)
 
210
        self.emit("new-project-loaded", project)
 
211
 
 
212
    def _projectManagerNewProjectFailed(self, projectManager, uri, exception):
 
213
        self.emit("new-project-failed", uri, exception)
 
214
 
 
215
    def _projectManagerClosingProject(self, projectManager, project):
 
216
        return self.emit("closing-project", project)
 
217
 
 
218
    def _projectManagerProjectClosed(self, projectManager, project):
 
219
        self.timelineLogObserver.stopObserving(project.timeline)
 
220
        self.projectLogObserver.stopObserving(project)
 
221
        self.current = None
 
222
        self.emit("project-closed", project)
 
223
 
 
224
class InteractivePitivi(Pitivi):
 
225
    usage = _("""
 
226
      %prog [-r OUTPUT_FILE] [PROJECT_FILE]
 
227
      %prog -p [PROJECT_FILE]
 
228
      %prog -i [-a] [MEDIA_FILE]...""")
 
229
 
 
230
    description = _("""Starts the video editor, optionally loading PROJECT_FILE. If
 
231
no project is given, %prog creates a new project.
 
232
Alternatively, when -i is specified, arguments are treated as clips to be
 
233
imported into the project. If -a is specified, these clips will also be added to
 
234
the end of the project timeline.
 
235
When -r is specified, the given project file is rendered without opening the GUI.""")
 
236
 
 
237
    import_help = _("""Import each MEDIA_FILE into the project.""")
 
238
 
 
239
    add_help = _("""Add each MEDIA_FILE to timeline after importing.""")
 
240
    debug_help = _("""Run pitivi in the Python Debugger""")
 
241
 
 
242
    no_ui_help = _("""Run pitivi with no gui""")
 
243
    render_help = _("""Render the given project file to OUTPUT_FILE with no GUI.""")
 
244
    preview_help = _("""Preview the given project file without the full UI.""")
 
245
 
 
246
    def __init__(self):
 
247
        Pitivi.__init__(self)
 
248
        self.mainloop = gobject.MainLoop()
 
249
        self.actioner = None
 
250
 
 
251
    def _newProjectLoaded(self, project):
 
252
        if self.render_output:
 
253
            # create renderer and set output file
 
254
            self.actioner = Renderer(self.current, pipeline=None, outfile=self.output_file)
 
255
        elif self.preview:
 
256
            # create previewer and set ui
 
257
            self.actioner = Previewer(self.current, pipeline=None, ui=self.gui)
 
258
            # hack to make the gtk.HScale seek slider UI behave properly
 
259
            self.gui._durationChangedCb(None, project.timeline.duration)
 
260
        if self.actioner:
 
261
            self.actioner.connect("eos", self._eosCb)
 
262
            # on error, all we need to do is shutdown which is the same as we do for EOS
 
263
            self.actioner.connect("error", self._eosCb)
 
264
            # configure the actioner and start acting!
 
265
            self.actioner.startAction()
 
266
 
 
267
    def run(self, argv):
 
268
        # check for dependencies
 
269
        if not self._checkDependencies():
 
270
            return
 
271
 
 
272
        # parse cmdline options
 
273
        parser = self._createOptionParser()
 
274
        options, args = parser.parse_args(argv)
 
275
 
 
276
        # if we aren't importing sources then n_args should be at most
 
277
        # 1 + parameters that take individual arguments
 
278
        n_args = 1
 
279
 
 
280
        if options.debug:
 
281
            sys.excepthook = self._excepthook
 
282
 
 
283
        # validate options
 
284
        self.render_output = options.render_output
 
285
        self.preview = options.preview
 
286
        if options.render_output:
 
287
            options.no_ui = True
 
288
            n_args += 1
 
289
 
 
290
        if options.render_output and options.preview:
 
291
            parser.error("-p and -r cannot be used simultaneously")
 
292
            return
 
293
 
 
294
        if options.import_sources and (options.render_output or options.preview):
 
295
            parser.error("-r or -p and -i are incompatible")
 
296
            return
 
297
 
 
298
        if not options.import_sources and options.add_to_timeline:
 
299
            parser.error("-a requires -i")
 
300
            return
 
301
 
 
302
        if not options.import_sources and ((options.render_output and len(args) != 2)
 
303
                    or len(args) > n_args):
 
304
            parser.error("invalid arguments")
 
305
            return
 
306
 
 
307
        if options.no_ui:
 
308
            self.gui = None
 
309
        elif options.preview:
 
310
            # init ui for previewing
 
311
            self.gui = PitiviViewer()
 
312
            self.window = gtk.Window()
 
313
            self.window.connect("delete-event", self._deleteCb)
 
314
            self.window.add(self.gui)
 
315
            self.window.show_all()
 
316
        else:
 
317
            # create the ui
 
318
            self.gui = PitiviMainWindow(self)
 
319
            self.gui.show()
 
320
 
 
321
        if not options.import_sources and args:
 
322
            index = 0
 
323
            if options.render_output:
 
324
                self.output_file = "file://%s" % os.path.abspath(args[index])
 
325
                index += 1
 
326
            # load a project file
 
327
            project = "file://%s" % os.path.abspath(args[index])
 
328
            self.projectManager.loadProject(project)
 
329
        else:
 
330
            # load the passed filenames, optionally adding them to the timeline
 
331
            # (useful during development)
 
332
            self.projectManager.newBlankProject()
 
333
            uris = ["file://" + urllib.quote(os.path.abspath(path)) for path in args]
 
334
            self.current.sources.connect("source-added",
 
335
                    self._sourceAddedCb, uris, options.add_to_timeline)
 
336
            self.current.sources.connect("discovery-error",
 
337
                    self._discoveryErrorCb, uris)
 
338
            self.current.sources.addUris(uris)
 
339
 
 
340
        # run the mainloop
 
341
        self.mainloop.run()
 
342
 
 
343
    def _deleteCb(self, unused_widget, unused_data):
 
344
        self.shutdown()
 
345
 
 
346
    def _eosCb(self, unused_obj):
 
347
        if self.gui is None:
 
348
            self.shutdown()
 
349
        elif self.window is not None:
 
350
            self.gui.seek(0)
 
351
 
 
352
    def shutdown(self):
 
353
        if Pitivi.shutdown(self):
 
354
            if self.gui:
 
355
                self.gui.destroy()
 
356
            self.mainloop.quit()
 
357
            return True
 
358
 
 
359
        return False
 
360
 
 
361
    def _createOptionParser(self):
 
362
        parser = OptionParser(self.usage, description=self.description)
 
363
        parser.add_option("-i", "--import", help=self.import_help,
 
364
                dest="import_sources", action="store_true", default=False)
 
365
        parser.add_option("-a", "--add-to-timeline", help=self.add_help,
 
366
                action="store_true", default=False)
 
367
        parser.add_option("-d", "--debug", help=self.debug_help,
 
368
                action="store_true", default=False)
 
369
        parser.add_option("-n", "--no-ui", help=self.no_ui_help,
 
370
                action="store_true", default=False)
 
371
        parser.add_option("-r", "--render", help=self.render_help,
 
372
                dest="render_output", action="store_true", default=False)
 
373
        parser.add_option("-p", "--preview", help=self.preview_help,
 
374
                action="store_true", default=False)
 
375
 
 
376
        return parser
 
377
 
 
378
 
 
379
    def _checkDependencies(self):
 
380
        missing_deps = initial_checks()
 
381
        if missing_deps:
 
382
            message, detail = missing_deps
 
383
            dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
 
384
                                       buttons=gtk.BUTTONS_OK)
 
385
            dialog.set_icon_name("pitivi")
 
386
            dialog.set_markup("<b>"+message+"</b>")
 
387
            dialog.format_secondary_text(detail)
 
388
            dialog.run()
 
389
 
 
390
            return False
 
391
 
 
392
        return True
 
393
 
 
394
    def _sourceAddedCb(self, sourcelist, factory,
 
395
            startup_uris, add_to_timeline):
 
396
        if self._maybePopStartupUri(startup_uris, factory.uri) \
 
397
                and add_to_timeline:
 
398
            self.action_log.begin("add clip")
 
399
            self.current.timeline.addSourceFactory(factory)
 
400
            self.action_log.commit()
 
401
 
 
402
    def _discoveryErrorCb(self, sourcelist, uri, error, debug, startup_uris):
 
403
        self._maybePopStartupUri(startup_uris, uri)
 
404
 
 
405
    def _maybePopStartupUri(self, startup_uris, uri):
 
406
        try:
 
407
            startup_uris.remove(uri)
 
408
        except ValueError:
 
409
            # uri is not a startup uri. This can happen if the user starts
 
410
            # importing sources while sources specified at startup are still
 
411
            # being processed. In practice this will never happen.
 
412
            return False
 
413
 
 
414
        if not startup_uris:
 
415
            self.current.sources.disconnect_by_function(self._sourceAddedCb)
 
416
            self.current.sources.disconnect_by_function(self._discoveryErrorCb)
 
417
 
 
418
        return True
 
419
 
 
420
    def _excepthook(self, exc_type, value, tback):
 
421
        import traceback
 
422
        import pdb
 
423
        traceback.print_tb(tback)
 
424
        pdb.post_mortem(tback)
 
425
 
 
426
def main(argv):
 
427
    ptv = InteractivePitivi()
 
428
    ptv.run(sys.argv[1:])