diff -ru --exclude=CVS pitivi_pub/bin/pitivi.in pitivi_rjb/bin/pitivi.in
--- pitivi_pub/bin/pitivi.in	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/bin/pitivi.in	2006-12-03 17:05:34.000000000 +0000
@@ -38,7 +38,8 @@
 
 def _add_pitivi_path():
     dir = os.path.dirname(os.path.abspath(__file__))
-    if os.path.exists(os.path.join(dir, 'CVS')):
+    if (os.path.exists(os.path.join(dir, 'CVS')) or 
+        os.path.exists(os.path.join(dir, '.svn'))):
         root = os.path.split(dir)[0]
 	localedir = os.path.join(root, 'locale')
     else:
diff -ru --exclude=CVS pitivi_pub/pitivi/configure.py.in pitivi_rjb/pitivi/configure.py.in
--- pitivi_pub/pitivi/configure.py.in	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/configure.py.in	2006-12-03 17:05:34.000000000 +0000
@@ -35,7 +35,8 @@
 def get_pixmap_dir():
     """ Returns the directory for program-only pixmaps """
     dir = os.path.dirname(os.path.abspath(__file__))
-    if os.path.exists(os.path.join(dir, 'CVS')):
+    if (os.path.exists(os.path.join(dir, 'CVS')) or
+        os.path.exists(os.path.join(dir, '.svn'))):
         root = dir
     else:
         root = PKGDATADIR
@@ -44,7 +45,8 @@
 def get_global_pixmap_dir():
     """ Returns the directory for global pixmaps (ex : application icon) """
     dir = os.path.dirname(os.path.abspath(__file__))
-    if os.path.exists(os.path.join(dir, 'CVS')):
+    if (os.path.exists(os.path.join(dir, 'CVS')) or
+        os.path.exists(os.path.join(dir, '.svn'))):
         root = string.join(dir.split('/')[:-1], '/')
     else:
         root = PIXMAPDIR
diff -ru --exclude=CVS pitivi_pub/pitivi/.cvsignore pitivi_rjb/pitivi/.cvsignore
--- pitivi_pub/pitivi/.cvsignore	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/.cvsignore	2006-12-03 17:05:34.000000000 +0000
@@ -18,3 +18,4 @@
 timeline.pyc
 utils.pyc
 pitivi.pyc
+pitivigstutils.pyc
Only in pitivi_rjb/pitivi/elements: .cvsignore
Only in pitivi_rjb/pitivi: pitivigstutils.pyc
diff -ru --exclude=CVS pitivi_pub/pitivi/pitivi.py pitivi_rjb/pitivi/pitivi.py
--- pitivi_pub/pitivi/pitivi.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/pitivi.py	2006-12-03 17:05:34.000000000 +0000
@@ -3,6 +3,7 @@
 #       pitivi/pitivi.py
 #
 # Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
+# Copyright (c) 2006, Richard Boulton <richard@tartarus.org>
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -40,35 +41,75 @@
 from gettext import gettext as _
 
 class Pitivi(gobject.GObject):
-    """
-    Pitivi's main class
+    """Pitivi's main class.
+
+    This class is mainly responsible for managing Pitivi projects, controling
+    construction and destruction of the UI, and running the mainloop.
 
-    Signals:
-      new-project : A new project has been loaded, the Project object is given
-      closing-project : Pitivi wishes to close the project, callbacks return False
-                if they don't want the project to be closed, True otherwise
-      not-project : The given uri is not a project file
+    Signals for other classes to listen for:
+     _ new-project-loaded : A new project has been loaded, the Project object
+       is the extra argument.
+     _ close-project-requested : Pitivi wishes to close the project, callbacks
+       should return False if they want the project not to be closed
+       immediately, True otherwise.
+     _ save-project-requested : Pitivi wishes to save a project, but doesn't
+       have a filename to save as.
+     _ confirm-overwrite-file-changed : The project has been requested to save
+       itself, but the project file has changed since the last time it was
+       saved (or, since it was loaded).  The path is supplied as an extra
+       argument to the signal.  Callbacks should return True if the existing
+       path should be overwritten.
+     _ open-project-failed : An attempt to load a project failed.  The uri of
+       the project is the first extra argument, and a string giving the reason
+       for the failure is the second argument.
+
+    Signals used internally:
+     _ request-new-project: Called to request opening of a new project
+       (replacing the current one).  No arguments.
+     _ request-load-project: Called to request opening of an existing project
+       (replacing the current one).  1 argument: the uri to open.
+     _ request-shutdown: Called to request shutdown of PiViTi.  No arguments.
 
     """
     __gsignals__ = {
-        "new-project" : ( gobject.SIGNAL_RUN_LAST,
-                          gobject.TYPE_NONE,
-                          (gobject.TYPE_PYOBJECT, )),
-        "closing-project" : ( gobject.SIGNAL_RUN_LAST,
-                              gobject.TYPE_BOOLEAN,
-                              (gobject.TYPE_PYOBJECT, )),
-        "not-project" : ( gobject.SIGNAL_RUN_LAST,
-                          gobject.TYPE_NONE,
-                          (gobject.TYPE_STRING, ))
+        "new-project-loaded" : ( gobject.SIGNAL_RUN_LAST,
+                                 gobject.TYPE_NONE,
+                                 (gobject.TYPE_PYOBJECT, )),
+        "close-project-requested" : ( gobject.SIGNAL_RUN_LAST,
+                                      gobject.TYPE_BOOLEAN,
+                                      (gobject.TYPE_PYOBJECT, )),
+        "save-project-requested" : ( gobject.SIGNAL_RUN_LAST,
+                                     gobject.TYPE_NONE,
+                                     ()),
+        "confirm-overwrite-file-changed" : ( gobject.SIGNAL_RUN_LAST,
+                                             gobject.TYPE_BOOLEAN,
+                                             (gobject.TYPE_STRING, )),
+        "open-project-failed" : ( gobject.SIGNAL_RUN_LAST,
+                                  gobject.TYPE_NONE,
+                                  (gobject.TYPE_STRING, gobject.TYPE_STRING, )),
+
+
+        "request-new-project" : ( gobject.SIGNAL_RUN_LAST,
+                                  gobject.TYPE_NONE,
+                                  ()),
+        "request-load-project" : ( gobject.SIGNAL_RUN_LAST,
+                                  gobject.TYPE_NONE,
+                                  ()),
+        "request-shutdown" : ( gobject.SIGNAL_RUN_LAST,
+                               gobject.TYPE_NONE,
+                               ()),
         }
 
     project = None
 
     def __init__(self, *args):
+        """Initialize pitivi, using the command line arguments.
+
         """
-        initialize pitivi with the command line arguments
-        """
-        gst.log("starting up pitivi...")
+        self.pendingAction = None
+        self.pendingActionArgs = None
+
+        gst.log("Starting up pitivi...")
         gobject.GObject.__init__(self)
 
         # store ourself in the instance global
@@ -77,58 +118,190 @@
         instance.PiTiVi = self
         
         # TODO parse cmd line arguments
+        # Note that we should still allow gstreamer cmd line args to work (eg,
+        # --gst-debug-level)
 
         self.playground = PlayGround()
         self.current = Project(_("New Project"))
+        self._connectToProject()
         self.effects = Magician()
         
         # we're starting a GUI for the time being
         self.gui = mainwindow.PitiviMainWindow()
         self.gui.show()
 
-    def loadProject(self, uri=None, filepath=None):
-        """ Load the given file through it's uri or filepath """
-        gst.info("uri:%s, filepath:%s" % (uri, filepath))
-        if not uri and not filepath:
-            self.emit("not-project", "")
+        self.connect_object("request-new-project", Pitivi.newProject, self)
+        self.connect_object("request-load-project", Pitivi.loadProjectByUri, self)
+        self.connect_object("request-shutdown", Pitivi.requestShutdown, self)
+
+    def _connectToProject(self):
+        """Create a new project, and connect to its signals.
+
+        """
+        self.current.connect("save-project-requested",
+                             self._saveProjectRequestedCb)
+        self.current.connect("confirm-overwrite-file-changed",
+                             self._confirmOverwriteFileChangedCb)
+
+    def _saveProjectRequestedCb(self, unused_project):
+        """Handle a save-project-requested signal from a project.
+
+        This simply passes the signal on to anyone listening for the equivalent
+        signal from this object.  This is useful because it prevents listeners
+        for the signal having to register with each new project.
+
+        """
+        return self.emit("save-project-requested")
+
+    def _confirmOverwriteFileChangedCb(self, project, uri):
+        """Handle a confirm-overwrite-file-changed signal from a project.
+
+        This simply passes the signal on to anyone listening for the equivalent
+        signal from this object.  This is useful because it prevents listeners
+        for the signal having to register with each new project.
+
+        """
+        return self.emit(signal, uri)
+
+    def newProject(self):
+        """Start up a new blank project.
+        
+        """
+        # If there's a running project we must close it first.
+        if not self._closeCurrentProject("request-new-project"):
             return
-        if filepath:
-            uri = "file://" + filepath
-        # is the given filepath a valid pitivi project
-        if not file_is_project(uri):
-            self.emit("not-project", uri)
+
+        self.playground.pause()
+        self.current = Project(_("New Project"))
+        self._connectToProject()
+        self.emit("new-project-loaded", self.current)
+
+    def loadProjectByFilepath(self, filepath):
+        """Load a project from a given filepath, replacing the current one.
+
+        `filepath` is the filepath to load the project from.
+        
+        """
+        self.loadProjectByUri("file://" + filepath)
+
+    def loadProjectByUri(self, uri):
+        """Load a project from a given URI, replacing the current one.
+
+        `uri` is the uri to load the project from.
+
+        """
+        # Close any current project
+        if not self._closeCurrentProject("request-load-project", uri):
+            return
+
+        gst.info("Loading project from %s" % uri)
+
+        # Check if the given filepath is a valid pitivi project
+        new_project = Project(uri=uri)
+        failure_reason = new_project.load()
+        if failure_reason is not None:
+            self.emit("open-project-failed", uri, failure_reason)
             return
-        # if current project, try to close it
-        if self._closeRunningProject():
-            self.current = Project(uri)
-            self.emit("new-project", self.current)
-
-    def _closeRunningProject(self):
-        """ close the current project """
-        gst.info("closing running project")
+
+        self.current = new_project
+        self._connectToProject()
+        self.emit("new-project-loaded", self.current)
+
+    def cancelPendingAction(self):
+        """Cancel the action which was pending.
+        
+        This is used to cancel the action which was in progress when request
+        to close the current project was denied (probably because the user
+        was presented with a question about whether to save the project or
+        not).
+
+        """
+        self.pendingAction = None
+        self.pendingActionArgs = None
+
+    def resumePendingAction(self):
+        """Resume the action which was pending.
+        
+        This is used to resume the action which was in progress when request
+        to close the current project was denied (probably because the user
+        was presented with a question about whether to save the project or
+        not).
+
+        If no action was pending (or the action has already been cancelled with
+        cancelPendingAction), this does nothing.
+
+        """
+        if self.pendingAction:
+            pendingAction = self.pendingAction
+            pendingActionArgs = self.pendingActionArgs
+            self.pendingAction = None
+            self.pendingActionArgs = None
+            self.emit(pendingAction, *pendingActionArgs)
+
+    def closeCurrentProjectForce(self):
+        """Force the current project to close.
+
+        This will close the current project immediately, even if it has unsaved
+        modifications.
+
+        """
+        gst.info("Closing project")
         if self.current:
-            if not self.emit("closing-project", self.current):
-                return False
             self.playground.pause()
             self.current = None
+
+    def _closeCurrentProject(self, pendingAction, *args):
+        """Close the current project.
+
+        If the project is successfully closed (or not open), returns True.
+        Otherwise, returns False.
+
+        pendingAction is the name of a signal to call, with the arguments
+        supplied in *args, if this method returns False, but the condition
+        preventing immediate closing is later resolved.
+        
+        """
+        if self.current is None:
+            return True
+
+        gst.info("Sending close-project-requested signal, with pendingAction=%s, args=%s" %
+                 (pendingAction, repr(args)))
+        self.pendingAction = pendingAction
+        self.pendingActionArgs = args
+        if not self.emit("close-project-requested", self.current):
+            return False
+
+        self.pendingAction = None
+        self.pendingActionArgs = None
+        self.closeCurrentProjectForce()
         return True
         
-    def newBlankProject(self):
-        """ start up a new blank project """
-        # if there's a running project we must close it
-        if self._closeRunningProject():
-            self.playground.pause()
-            self.current = Project(_("New Project"))
-            self.emit("new-project", self.current)
+    def requestShutdown(self):
+        """Request that PiTiVi shuts itself down.
+
+        This request may result in PiTiVi shutting down immediately, or may
+        result in a "closing-project" signal being sent (if there are unsaved
+        changes).
+
+        """
+        gst.info("shut down requested")
+        if not self._closeCurrentProject("request-shutdown"):
+            return True
 
-    def shutdown(self):
-        """ close PiTiVi """
-        gst.debug("shutting down")
-        if not self._closeRunningProject():
-            return
         self.playground.shutdown()
         gst.debug("Exiting main loop")
         gtk.main_quit()
+        return False
+
+    def forceShutdown(self):
+        """Force PiTiVi to itself down immediately.
+
+        """
+        gst.info("shutting down")
+        self.closeCurrentProjectForce()
+        self.playground.shutdown()
+        gst.info("Exiting main loop")
+        gtk.main_quit()
 
         
 def main(argv):
diff -ru --exclude=CVS pitivi_pub/pitivi/project.py pitivi_rjb/pitivi/project.py
--- pitivi_pub/pitivi/project.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/project.py	2006-12-03 17:05:34.000000000 +0000
@@ -25,6 +25,9 @@
 """
 
 import os.path
+import tempfile
+import time
+import math
 import gobject
 import gst
 from timeline import Timeline
@@ -36,20 +39,38 @@
 from gettext import gettext as _
 
 class Project(gobject.GObject):
-    """ The base class for PiTiVi projects """
+    """The base class for PiTiVi projects
 
+    Signals:
+     _ save-project-requested : The project has been requested to
+       save itself, but needs a URI to save to, or other settings.
+     _ confirm-overwrite-file-changed : The project has been requested to save
+       itself, but the project file has changed since the last time it was
+       saved (or, since it was loaded).  The path is supplied as an extra
+       argument to the signal.  Callbacks should return True if the existing
+       path should be overwritten.
+
+    """
     __gsignals__ = {
         "settings-changed" : ( gobject.SIGNAL_RUN_LAST,
                                gobject.TYPE_NONE,
-                               (  ))
+                               ()),
+        "save-project-requested" : ( gobject.SIGNAL_RUN_LAST,
+                                     gobject.TYPE_NONE,
+                                     ()),
+        "confirm-overwrite-file-changed" : ( gobject.SIGNAL_RUN_LAST,
+                                             gobject.TYPE_BOOLEAN,
+                                             (gobject.TYPE_STRING, )),
         }
 
     def __init__(self, name="", uri=None):
+        """Initialise a new project.
+
+        Even if a URI is specified, this does not load the project.
+        Loading is performed only when the load method is explicitly called.
+
         """
-        name : the name of the project
-        uri : the uri of the project
-        """
-        gst.log("name:%s, uri:%s" % (name, uri))
+        gst.log("Making new project, name:%s, uri:%s" % (name, uri))
         gobject.GObject.__init__(self)
         self.name = name
         self.settings = None
@@ -59,15 +80,153 @@
         self.timeline = None
         self.timelinebin = None
         self.settingssigid = 0
-        self._load()
-
-    def _load(self):
-        """ loads the project from a file """
-        if self.timeline:
-            return
+        self.modified = False
+        self.last_mtime = None # mtime of saved project
+        self.first_modification_time = None
         self.timeline = Timeline(self)
-        if self.uri:
-            raise NotImplementedError
+        self.clearModificationState()
+
+    def clearModificationState(self):
+        """Clear the modified state.
+
+        This is used, for example, after a save to indicate that there are no
+        longer unsaved modifications.
+
+        """
+        self.modified = False
+        self.first_modification_time = None
+
+    def registerModification(self):
+        """Register that a modification has occurred.
+
+        TODO - expand this into a framework which also handles undo/redo.
+
+        """
+        if not self.modified:
+            self.modified = True
+            self.first_modification_time = time.time()
+
+    def secondsSinceFirstModification(self):
+        """Return the number of seconds since the first modification occurred.
+
+        This returns an integer number of seconds, rounded up, or 0 if no
+        modifications have occurred.
+
+        """
+        if self.first_modification_time is None:
+            return 0
+        return int(math.ceil(time.time() - self.first_modification_time))
+
+    def load(self):
+        """Loads the project.
+
+        The project's URI must be set before this is called.
+
+        """
+        assert self.uri is not None
+        assert self.uri.startswith("file:/")
+        filename = self.uri[5:]
+        self.last_mtime = os.path.getmtime(filename)
+
+        # TODO
+        raise NotImplementedError
+
+        self.clearModificationState()
+
+    def _writeToFd(self, fd):
+        """Write the project to the given file descriptor.
+
+        """
+        assert self.timeline is not None
+        self.timeline.writeToFd(fd)
+
+    def _save(self, uri):
+        """Internal save function.
+        
+        """
+        assert uri is not None
+        assert uri.startswith("file:/")
+        newfilename = uri[5:]
+
+        currentfilename = None
+        if (self.uri is not None and
+            self.uri.startswith("file:/")):
+            currentfilename = self.uri[5:]
+
+        # If we're saving to the same URI, check if it's been changed
+        # externally since the last save, and ask the UI what to do if so.
+        if (currentfilename is not None and
+            os.path.exists(currentfilename) and
+            os.path.exists(newfilename) and
+            os.path.samefile(currentfilename, newfilename) and
+            self.last_mtime != os.path.getmtime(newfilename)):
+            if not self.emit('confirm-overwrite-file-changed', uri):
+                return
+
+        targetdir = os.path.dirname(newfilename)
+        targetname = os.path.basename(newfilename)
+        assert targetname != ''
+        (tmpfd, tmpfilename) = tempfile.mkstemp('.tmp', targetname, targetdir)
+        try:
+            fd = file(tmpfilename, "wb")
+            try:
+                self._writeToFd(fd)
+            finally:
+                fd.close()
+            try:
+                os.remove(newfilename)
+            except OSError:
+                pass
+            os.rename(tmpfilename, newfilename)
+        except:
+            try:
+                os.remove(tmpfilename)
+            except OSError:
+                pass
+            raise
+
+        self.last_mtime = os.path.getmtime(newfilename)
+        self.clearModificationState()
+
+    def save(self):
+        """Save the project.
+
+        If the project has a URI and all necessary settings, this immediately
+        saves the project.
+
+        If the project does not have a URI or some of the necessary settings,
+        this emits the save-project-requested signal, and doesn't save the
+        project.
+
+        """
+        if self.uri is None:
+            self.emit('save-project-requested')
+        else:
+            self._save(self.uri)
+
+    def saveAs(self, uri):
+        """Saves the project to the given URI.
+        
+        This also sets the project URI to the given URI, so future calls to
+        save() will use this URI.
+        
+        """
+        assert uri is not None
+        assert uri.startswith("file:/")
+
+        if (self.uri is not None and
+            self.uri.startswith("file:/") and
+            os.path.samefile(self.uri[5:], uri[5:])):
+            self.save()
+        else:
+            self._save(uri)
+            self.uri = uri
+
+    def hasUnsavedModifications(self):
+        """Return True iff the project has been modified since saving.
+
+        """
+        return self.modified
 
     def getBin(self):
         """ returns the SmartTimelineBin of the project """
@@ -77,18 +236,6 @@
             self.timelinebin = SmartTimelineBin(self)
         return self.timelinebin
 
-    def _save(self, filename):
-        """ internal save function """
-        # TODO
-        pass
-
-    def save(self):
-        """ Saves the project to the project's current file """
-        self._save(self.uri)
-
-    def saveAs(self, filename):
-        """ Saves the project to the given file name """
-        self._save(filename)
 
     # setting methods
     def _settingsChangedCb(self, settings):
diff -ru --exclude=CVS pitivi_pub/pitivi/sourcelist.py pitivi_rjb/pitivi/sourcelist.py
--- pitivi_pub/pitivi/sourcelist.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/sourcelist.py	2006-12-03 17:05:34.000000000 +0000
@@ -38,6 +38,9 @@
                 A file was removed from the SourceList
     _ tmp-is-ready (FileSourceFactory) :
                 The temporary uri given to the SourceList is ready to use.
+    _ not_media_file (string : uri, string : reason) :
+                Location given by uri was found not to be a media file, for
+                given reason.
     """
 
     __gsignals__ = {
@@ -99,6 +102,7 @@
             return
         self.sources[uri] = None
         self.discoverer.addFile(uri)
+        self.project.registerModification()
 
     def addUris(self, uris):
         """ Add the list of uris to the list of sources, they will be discovered """
@@ -109,6 +113,7 @@
                 self.sources[uri] = None
                 rlist.append(uri)
         self.discoverer.addFiles(rlist)
+        self.project.registerModification()
 
     def addTmpUri(self, uri):
         """ Adds a temporary uri, will not be saved """
@@ -129,6 +134,7 @@
                 rmuri.append(uri)
         for uri in rmuri:
             del self[uri]
+        self.project.registerModification()
 
     def _finishedAnalyzingCb(self, unused_discoverer, factory):
         # callback from finishing analyzing factory
diff -ru --exclude=CVS pitivi_pub/pitivi/timeline.py pitivi_rjb/pitivi/timeline.py
--- pitivi_pub/pitivi/timeline.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/timeline.py	2006-12-03 17:05:34.000000000 +0000
@@ -95,6 +95,9 @@
         self.audiocomp.gnlobject.connect("pad-removed", self._removedAudioPadCb)
         self.videocomp.gnlobject.connect("pad-removed", self._removedVideoPadCb)
 
+    def writeToFd(self, fd):
+        fd.write(self.videocomp.serialise())
+
     def _newAudioPadCb(self, unused_audiocomp, pad):
         self.timeline.add_pad(gst.GhostPad("asrc", pad))
 
@@ -126,7 +129,6 @@
             s.audiodepth = a.audiodepth
         return s
 
-
 class TimelineObject(gobject.GObject):
     """
     Base class for all timeline objects
@@ -281,7 +283,7 @@
                 self.duration = long(duration)
         #if not start == -1 or not duration == -1:
         self.emit("start-duration-changed", self.start, self.duration)
-            
+
 
         
 class TimelineSource(TimelineObject):
@@ -395,6 +397,16 @@
     def getExportSettings(self):
         return self.factory.getExportSettings()
 
+    def serialise(self):
+        """Save the composition to a filedescriptor.
+
+        """
+        result = []
+        result.append("start=%d," % self.start)
+        result.append("duration=%d," % self.duration)
+        result.append("filename=%s," % self.factory.name)
+        return u''.join(result)
+
 
 class TimelineLiveSource(TimelineSource):
     """
@@ -931,6 +943,19 @@
                 return self._autoVideoSettings()
         
 
+    def serialise(self):
+        """Save the composition to a filedescriptor.
+
+        """
+        result = []
+        for (min_priority, max_priority, sourcelist) in self.sources:
+            result.append("(%d, %d, [" % (min_priority, max_priority))
+            for source in sourcelist:
+                result.append("%s" % source.serialise())
+            result.append("])")
+        return u''.join(result)
+            
+
 class TimelineEffect(TimelineObject):
     """
     Base class for effects (1->n input(s))
Only in pitivi_rjb/pitivi/ui: confirmclosedialog.glade
Only in pitivi_rjb/pitivi/ui: confirmclosedialog.py
Only in pitivi_rjb/pitivi/ui: confirmclosedialog.pyc
diff -ru --exclude=CVS pitivi_pub/pitivi/ui/.cvsignore pitivi_rjb/pitivi/ui/.cvsignore
--- pitivi_pub/pitivi/ui/.cvsignore	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/ui/.cvsignore	2006-12-03 17:05:34.000000000 +0000
@@ -19,4 +19,6 @@
 timelineobjects.pyc
 tracklayer.pyc
 viewer.pyc
-
+confirmclosedialog.pyc
+filelisterrordialog.pyc
+signalgroup.pyc
Only in pitivi_rjb/pitivi/ui: filelisterrordialog.glade
Only in pitivi_rjb/pitivi/ui: filelisterrordialog.py
Only in pitivi_rjb/pitivi/ui: filelisterrordialog.pyc
diff -ru --exclude=CVS pitivi_pub/pitivi/ui/mainwindow.py pitivi_rjb/pitivi/ui/mainwindow.py
--- pitivi_pub/pitivi/ui/mainwindow.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/ui/mainwindow.py	2006-12-03 17:05:34.000000000 +0000
@@ -40,11 +40,93 @@
 from projectsettings import ProjectSettingsDialog
 from pitivi.configure import pitivi_version, APPNAME
 
+from filelisterrordialog import FileListErrorDialog
+from confirmclosedialog import ConfirmCloseDialog
+
 D_G_INTERFACE = "/desktop/gnome/interface"
 
 for gconf_dir in (D_G_INTERFACE,):
     gconf.client_get_default ().add_dir (gconf_dir, gconf.CLIENT_PRELOAD_NONE)
 
+class ProjectSaveWindow:
+    def __init__(self):
+        self.dialog = None
+        self._lastFolder = None
+
+    def isOpen(self):
+        return self.dialog is not None
+
+    def open(self):
+        self.dialog = gtk.FileChooserDialog(_("Choose a file to save as"), None,
+                                            gtk.FILE_CHOOSER_ACTION_SAVE,
+                                            (gtk.STOCK_SAVE, gtk.RESPONSE_OK,
+                                             gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
+        self.dialog.set_do_overwrite_confirmation(True)
+
+        self.dialog.set_default_response(gtk.RESPONSE_OK)
+        self.dialog.set_modal(True)
+        if self._lastFolder is not None:
+            self.dialog.set_current_folder(self._lastFolder)
+        self.dialog.connect("key-press-event", self._keyPressEventCb)
+        self.dialog.connect('response', self._responseCb)
+        self.dialog.connect('close', self._closeCb)
+        self.dialog.show()
+
+    def _keyPressEventCb(self, unused_widget, event):
+        if gtk.gdk.keyval_name(event.keyval) in ['Escape']:
+            self.dialog.response(gtk.RESPONSE_CANCEL)
+
+    def _responseCb(self, dialogbox, response):
+        gst.debug("response:%r" % response)
+        if response == gtk.RESPONSE_OK:
+            self._lastFolder = dialogbox.get_current_folder()
+            instance.PiTiVi.current.saveAs(dialogbox.get_uri())
+        dialogbox.destroy()
+        self.dialog = None
+
+    def _closeCb(self, dialogbox):
+        gst.debug("closing")
+        self.dialog = None
+
+class ProjectLoadWindow:
+    def __init__(self):
+        self.dialog = None
+        self._lastFolder = None
+
+    def isOpen(self):
+        return self.dialog is not None
+
+    def open(self):
+        self.dialog = gtk.FileChooserDialog(_("Choose a project file to load"), None,
+                                            gtk.FILE_CHOOSER_ACTION_OPEN,
+                                            (gtk.STOCK_OPEN, gtk.RESPONSE_OK,
+                                             gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
+
+        self.dialog.set_default_response(gtk.RESPONSE_OK)
+        self.dialog.set_modal(True)
+        if self._lastFolder is not None:
+            self.dialog.set_current_folder(self._lastFolder)
+        self.dialog.connect("key-press-event", self._keyPressEventCb)
+        self.dialog.connect('response', self._responseCb)
+        self.dialog.connect('close', self._closeCb)
+        self.dialog.show()
+
+    def _keyPressEventCb(self, unused_widget, event):
+        if gtk.gdk.keyval_name(event.keyval) in ['Escape']:
+            self.dialog.response(gtk.RESPONSE_CANCEL)
+
+    def _responseCb(self, dialogbox, response):
+        gst.debug("response:%r" % response)
+        if response == gtk.RESPONSE_OK:
+            self._lastFolder = dialogbox.get_current_folder()
+            instance.PiTiVi.loadProjectByUri(dialogbox.get_uri())
+        dialogbox.destroy()
+        self.dialog = None
+
+    def _closeCb(self, dialogbox):
+        gst.debug("closing")
+        self.dialog = None
+
 class PitiviMainWindow(gtk.Window):
     """
     Pitivi's main window
@@ -60,17 +142,24 @@
 
         self.isFullScreen = False
         self.errorDialogBox = None
+        self.openProjectErrorDialogBox = None
+        self.confirmCloseDialogBox = None
+
+        self._saveAsDialogBox = None
+        self._loadFromDialogBox = None
         
-        instance.PiTiVi.connect("new-project", self._newProjectCb)
-        instance.PiTiVi.connect("closing-project", self._closingProjectCb)
-        instance.PiTiVi.connect("not-project", self._notProjectCb)
+        instance.PiTiVi.connect("new-project-loaded", self._newProjectCb)
+        instance.PiTiVi.connect("close-project-requested", self._closingProjectCb)
+        instance.PiTiVi.connect("save-project-requested", self._saveProjectRequestedCb)
+        instance.PiTiVi.connect("confirm-overwrite-file-changed", self._confirmOverwriteChangedCb)
+        instance.PiTiVi.connect("open-project-failed", self._notProjectCb)
         instance.PiTiVi.playground.connect("error", self._playGroundErrorCb)
         self.show_all()
 
     def _setActions(self):
         """ sets up the GtkActions """
         self.actions = [
-            ("NewProject", gtk.STOCK_NEW, None, None, _("Create a new project"), self._newProjectCb),
+            ("NewProject", gtk.STOCK_NEW, None, None, _("Create a new project"), self._createNewProjectCb),
             ("OpenProject", gtk.STOCK_OPEN, None, None, _("Open an existing project"), self._openProjectCb),
             ("SaveProject", gtk.STOCK_SAVE, None, None, _("Save the current project"), self._saveProjectCb),
             ("SaveProjectAs", gtk.STOCK_SAVE_AS, None, None, _("Save the current project"), self._saveProjectAsCb),
@@ -93,22 +182,12 @@
         self.actiongroup.add_actions(self.actions)
         self.actiongroup.add_toggle_actions(self.toggleactions)
         
-        # deactivating non-functional actions
-        # FIXME : reactivate them
-        for action in self.actiongroup.list_actions():
-            if action.get_name() in ["ProjectSettings", "Quit", "File", "Help",
-                                     "About", "View", "FullScreen", "ImportSources",
-                                     "ImportSourcesFolder", "AdvancedView"]:
-                action.set_sensitive(True)
-            else:
-                action.set_sensitive(False)
-                
         self.uimanager = gtk.UIManager()
         self.add_accel_group(self.uimanager.get_accel_group())
         self.uimanager.insert_action_group(self.actiongroup, 0)
         self.uimanager.add_ui_from_file(os.path.join(os.path.dirname(os.path.abspath(__file__)), "actions.xml"))
 
-        self.connect("key-press-event", self._keyPressEventCb)
+        self.connect_after("key-press-event", self._keyPressEventCb)
 
     def _createUi(self):
         """ Create the graphical interface """
@@ -116,6 +195,7 @@
         self.set_geometry_hints(min_width=800, min_height=600)
 
         self.connect("destroy", self._destroyCb)
+        self.connect("delete-event", self._deleteCb)
 
         vbox = gtk.VBox(False, 5)
         self.add(vbox)
@@ -161,14 +241,10 @@
             self.viewer.window.unfullscreen()
             self.isFullScreen = False
 
-    ## PlayGround callback
-
-    def _errorMessageResponseCb(self, dialogbox, unused_response):
-        dialogbox.hide()
-        dialogbox.destroy()
-        self.errorDialogBox = None
-
     def _playGroundErrorCb(self, unused_playground, error, detail):
+        """Display an error from the playground.
+
+        """
         if self.errorDialogBox:
             return
         self.errorDialogBox = gtk.MessageDialog(None, gtk.DIALOG_MODAL,
@@ -176,17 +252,25 @@
                                                 gtk.BUTTONS_OK,
                                                 None)
         self.errorDialogBox.set_markup("<b>%s</b>" % error)
-        self.errorDialogBox.connect("response", self._errorMessageResponseCb)
         if detail:
             self.errorDialogBox.format_secondary_text(detail)
+
+        def responseCb(dialogbox, unused_response, mainwindow):
+            dialogbox.hide()
+            dialogbox.destroy()
+            mainwindow.errorDialogBox = None
+        self.errorDialogBox.connect("response", responseCb, self)
+
         self.errorDialogBox.show()
 
 
     ## UI Callbacks
 
     def _destroyCb(self, unused_widget, data=None):
-        instance.PiTiVi.shutdown()
+        instance.PiTiVi.forceShutdown()
 
+    def _deleteCb(self, unused_widget, data=None):
+        return instance.PiTiVi.requestShutdown()
 
     def _keyPressEventCb(self, unused_widget, event):
         if gtk.gdk.keyval_name(event.keyval) in ['f', 'F', 'F11']:
@@ -194,24 +278,30 @@
 
     ## Toolbar/Menu actions callback
         
-    def _newProjectCb(self, unused_action):
-        instance.PiTiVi.new_blank_project()
+    def _createNewProjectCb(self, unused_action):
+        instance.PiTiVi.newProject()
 
     def _openProjectCb(self, unused_action):
-        raise NotImplementedError
+        if self._loadFromDialogBox is None:
+           self._loadFromDialogBox = ProjectLoadWindow()
+        if not self._loadFromDialogBox.isOpen():
+            self._loadFromDialogBox.open()
 
     def _saveProjectCb(self, unused_action):
-        raise NotImplementedError
+        instance.PiTiVi.current.save()
 
     def _saveProjectAsCb(self, unused_action):
-        raise NotImplementedError
+        if self._saveAsDialogBox is None:
+           self._saveAsDialogBox = ProjectSaveWindow()
+        if not self._saveAsDialogBox.isOpen():
+            self._saveAsDialogBox.open()
 
     def _projectSettingsCb(self, unused_action):
         l = ProjectSettingsDialog(self, instance.PiTiVi.current)
         l.show()
 
     def _quitCb(self, unused_action):
-        instance.PiTiVi.shutdown()
+        instance.PiTiVi.requestShutdown()
 
     def _fullScreenCb(self, unused_action):
         self.toggleFullScreen()
@@ -222,9 +312,6 @@
         else:
             self.timeline.showSimpleView()
 
-    def _aboutResponseCb(self, dialog, unused_response):
-        dialog.destroy()
-
     def _aboutCb(self, unused_action):
 	abt = gtk.AboutDialog()
 	abt.set_name(APPNAME)
@@ -232,11 +319,14 @@
 	abt.set_website("http://www.pitivi.org/")
 	authors = ["Edward Hervey <edward@fluendo.com>","",_("Contributors:"),
                    "Christophe Sauthier <christophe.sauthier@gmail.com> (i18n)",
-                   "Laszlo Pandy <laszlok2@gmail.com> (UI)", ]
+                   "Laszlo Pandy <laszlok2@gmail.com> (UI)",
+                   "Richard Boulton <richard@tartarus.org>",]
 	abt.set_authors(authors)
 	abt.set_license(_("GNU Lesser General Public License\nSee http://www.gnu.org/copyleft/lesser.html for more details"))
         abt.set_icon_from_file(configure.get_global_pixmap_dir() + "/pitivi.png")
-        abt.connect("response", self._aboutResponseCb)
+        def responseCb(dialog, unused_response):
+            dialog.destroy()
+        abt.connect("response", responseCb)
 	abt.show()
 
     def _importSourcesCb(self, unused_action):
@@ -248,16 +338,95 @@
     ## PiTiVi main object callbacks
 
     def _newProjectCb(self, pitivi, project):
-        raise NotImplementedError
+        # TODO - handle anything that needs setting for a new project (eg,
+        # window title).
+        pass
 
     def _closingProjectCb(self, pitivi, project):
-        # Return True if we accept the project being close
-        # if we want to save it before it being closed, we must
-        #   do so
+        """Called when a request to close the project has been made.
+
+        Returns True if we accept the project being closed, and False if
+        we don't (yet).
+
+        """
+        if (project.hasUnsavedModifications() and
+            project.secondsSinceFirstModification() > 30):
+            # Gnome HIG says "Present a confirmation alert when the user's
+            # command may ... take more than 30 seconds of user effort to
+            # recover from if it was selected in error."
+            if not self.confirmCloseDialogBox:
+                self.confirmCloseDialogBox = ConfirmCloseDialog(project)
+                def closeCb(dialog, mainwindow, pitivi):
+                    dialog.destroy()
+                    mainwindow.confirmCloseDialogBox = None
+                    pitivi.cancelPendingAction()
+                self.confirmCloseDialogBox.connect('close', closeCb, self, pitivi)
+
+                def responseCb(dialog, response, mainwindow, pitivi):
+                    dialog.destroy()
+                    mainwindow.confirmCloseDialogBox = None
+                    if response == gtk.RESPONSE_OK:
+                        # Save the project
+                        pitivi.current.save()
+                        pitivi.resumePendingAction()
+                    elif response == gtk.RESPONSE_CLOSE:
+                        # Close the project without saving.
+                        pitivi.closeCurrentProjectForce()
+                        pitivi.resumePendingAction()
+                    elif response == gtk.RESPONSE_CANCEL:
+                        # Don't close the project
+                        pitivi.cancelPendingAction()
+                    else:
+                        # Unknown - probably because the dialog was closed.
+                        # Treat as a cancel.
+                        pitivi.cancelPendingAction()
+                self.confirmCloseDialogBox.connect('response', responseCb, self, pitivi)
+
+                self.confirmCloseDialogBox.show()
+            return False
 
-        # For the time being we always accept it being closed
         return True
 
-    def _notProjectCb(self, pitivi, uri):
-        raise NotImplementedError
-    
+    def _saveProjectRequestedCb(self, pitivi):
+        """Called when a save was requested, but a filename is needed.
+
+        """
+        if self._saveAsDialogBox is None:
+           self._saveAsDialogBox = ProjectSaveWindow()
+        if not self._saveAsDialogBox.isOpen():
+            self._saveAsDialogBox.open()
+
+    def _confirmOverwriteChangedCb(self, pitivi, project):
+	"""Called when saving a project, but the project file has changed since
+        we last saved or loaded.
+
+        Returns True if we accept the file being overwritten, and False if
+        we don't (yet).
+
+        """
+        print "Confirm overwrite callback: %s" % reason
+        return False
+
+    def _notProjectCb(self, pitivi, uri, reason):
+        """Called when opening a project file failed.
+
+        """
+        # popup a dialog box and fill up with problems
+        if not self.openProjectErrorDialogBox:
+            # construct and display it
+            self.openProjectErrorDialogBox = FileListErrorDialog(_("Error with project files"),
+                                                                 _("The following project files couldn't be opened."))
+
+            def closeCb(dialog, mainwindow):
+                dialog.destroy()
+                mainwindow.openProjectErrorDialogBox = None
+            self.openProjectErrorDialogBox.connect('close', closeCb, self)
+
+            def responseCb(dialog, unused_response, mainwindow):
+                dialog.destroy()
+                mainwindow.openProjectErrorDialogBox = None
+            self.openProjectErrorDialogBox.connect('response', responseCb, self)
+
+            self.openProjectErrorDialogBox.show()
+        self.openProjectErrorDialogBox.addFailedFile(uri, reason)
+
diff -ru --exclude=CVS pitivi_pub/pitivi/ui/Makefile.am pitivi_rjb/pitivi/ui/Makefile.am
--- pitivi_pub/pitivi/ui/Makefile.am	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/ui/Makefile.am	2006-12-03 17:05:34.000000000 +0000
@@ -6,29 +6,37 @@
 	complexsource.py	\
 	complextimeline.py	\
 	complexinterface.py	\
-	exportsettingswidget.py \
+	confirmclosedialog.py	\
+	exportsettingswidget.py	\
+	filelisterrordialog.py	\
 	glade.py		\
 	gstwidget.py		\
+	infolayer.py		\
+	layerwidgets.py		\
 	mainwindow.py		\
+	plumber.py		\
 	projectsettings.py	\
+	ruler.py		\
+	signalgroup.py		\
 	sourcefactories.py	\
 	timeline.py		\
 	timelineobjects.py	\
-	viewer.py		\
-	ruler.py		\
-	layerwidgets.py		\
 	tracklayer.py		\
-	infolayer.py		\
-	plumber.py
+	viewer.py
 
 ui_DATA =				\
 	actions.xml			\
-	discoverererrordialog.glade	\
+	confirmclosedialog.glade	\
 	elementsettingsdialog.glade	\
 	encodingdialog.glade		\
 	exportsettingswidget.glade	\
+	filelisterrordialog.glade	\
+	projectopenerrordialog.glade	\
 	projectsettings.glade
 
+
+
+
 clean-local:
 	rm -rf *.pyc *.pyo
 
Only in pitivi_rjb/pitivi/ui: projectopenerrordialog.glade
Only in pitivi_pub/pitivi/ui: projectsettings.glade
Only in pitivi_rjb/pitivi/ui: signalgroup.py
Only in pitivi_rjb/pitivi/ui: signalgroup.pyc
diff -ru --exclude=CVS pitivi_pub/pitivi/ui/sourcefactories.py pitivi_rjb/pitivi/ui/sourcefactories.py
--- pitivi_pub/pitivi/ui/sourcefactories.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/ui/sourcefactories.py	2006-12-03 17:05:34.000000000 +0000
@@ -36,7 +36,9 @@
 import pitivi.instance as instance
 import pitivi.dnd as dnd
 from pitivi.configure import get_pixmap_dir
-from glade import GladeWindow
+
+from signalgroup import SignalGroup
+from filelisterrordialog import FileListErrorDialog
 
 from gettext import gettext as _
 
@@ -181,13 +183,11 @@
         # Start up with tree view
         self.scrollwin.add(self.treeview)
 
-        # callbacks from discoverer
-        # TODO : we must remove and reset the callbacks when changing project
-        instance.PiTiVi.current.sources.connect("file_added", self._fileAddedCb)
-        instance.PiTiVi.current.sources.connect("file_removed", self._fileRemovedCb)
-        instance.PiTiVi.current.sources.connect("not_media_file", self._notMediaFileCb)
-
-        instance.PiTiVi.connect("new-project", self._newProjectCb)
+        # Connect to project.  We must remove and reset the callbacks when
+        # changing project.
+        self.project_signals = SignalGroup()
+        self._connectToProject(instance.PiTiVi.current)
+        instance.PiTiVi.connect("new-project-loaded", self._newProjectCb)
 
         # default pixbufs
         pixdir = get_pixmap_dir()
@@ -213,6 +213,17 @@
         # Error dialog box
         self.errorDialogBox = None
 
+    def _connectToProject(self, project):
+        """Connect signal handlers to a project.
+
+        This first disconnects any handlers connected to an old project.
+        If project is None, this just disconnects any connected handlers.
+
+        """
+        self.project_signals.connect(project.sources, "file_added", None, self._fileAddedCb)
+        self.project_signals.connect(project.sources, "file_removed", None, self._fileRemovedCb)
+        self.project_signals.connect(project.sources, "not_media_file", None, self._notMediaFileCb)
+
     def showImportSourcesDialog(self, select_folders=False):
         if self._importDialog:
             return
@@ -303,10 +314,23 @@
         # popup a dialog box and fill up with reasons
         if not self.errorDialogBox:
             # construct the dialog but leave it hidden
-            self.errorDialogBox = DiscovererErrorDialog()
-            self.errorDialogBox.connect('close', self._errorDialogBoxCloseCb)
-            self.errorDialogBox.connect('response', self._errorDialogBoxResponseCb)
+            self.errorDialogBox = FileListErrorDialog(
+                _("Error while analyzing files"),
+                _("The following files weren't discovered properly.")
+            )
+
+            def closeCb(dialog, sourcelist):
+                sourcelist.errorDialogBox.destroy()
+                sourcelist.errorDialogBox = None
+            self.errorDialogBox.connect('close', closeCb, self)
+
+            def responseCb(dialog, unused_response, sourcelist):
+                sourcelist.errorDialogBox.destroy()
+                sourcelist.errorDialogBox = None
+            self.errorDialogBox.connect('response', responseCb, self)
+
             self.errorDialogBox.hide()
+
         if not self.errorDialogBox.isVisible() and not self.errorDialogButton.parent:
             # show the button that will open the dialog
             self.bothbox.pack_start(self.errorDialogButton, expand=False)
@@ -314,17 +338,6 @@
         self.errorDialogBox.addFailedFile(uri, reason)
 
 
-    ## Error Dialog Box callbacks
-
-    def _errorDialogBoxCloseCb(self, unused_dialog):
-        self.errorDialogBox.destroy()
-        self.errorDialogBox = None
-
-    def _errorDialogBoxResponseCb(self, unused_dialog, unused_response):
-        self.errorDialogBox.destroy()
-        self.errorDialogBox = None
-
-
     ## Import Sources Dialog Box callbacks
         
     def _dialogBoxResponseCb(self, dialogbox, response, select_folders):
@@ -391,6 +404,8 @@
         instance.PiTiVi.playground.playTemporaryFilesourcefactory(factory)
 
     def _newProjectCb(self, unused_pitivi, project):
+        assert(instance.PiTiVi.current == project)
+
         # clear the storemodel
         self.storemodel.clear()
 
@@ -417,9 +432,8 @@
                 # FIXME : update with new table structure (icon, infotext, objectfactory, uri
                 self.storemodel.append([thumbnail, name, factory, factory.name,
                                 "<b>%s</b>" % beautify_length(factory.length)])
-        
-        instance.PiTiVi.current.sources.connect("file_added", self._fileAddedCb)
-        instance.PiTiVi.current.sources.connect("file_removed", self._fileRemovedCb)
+
+        self._connectToProject(project)
 
 
     ## Drag and Drop
@@ -551,59 +565,3 @@
         self.iconview = gtk.IconView()
         self.treeview = gtk.TreeView()
         self.pack_start(self.iconview)
-
-
-class DiscovererErrorDialog(GladeWindow):
-    """ Dialog box for showing errors from discovering files """
-    glade_file = "discoverererrordialog.glade"
-    __gsignals__ = {
-        'close': (gobject.SIGNAL_RUN_LAST,
-                  gobject.TYPE_NONE,
-                  ( )),
-        'response': (gobject.SIGNAL_RUN_LAST,
-                     gobject.TYPE_NONE,
-                     (gobject.TYPE_PYOBJECT, ))
-        }
-
-    def __init__(self):
-        GladeWindow.__init__(self)
-        self.window.set_modal(False)
-        self.treeview = self.widgets["treeview"]
-        self.window.set_geometry_hints(min_width=400, min_height=300)
-        self._setUpTreeView()
-
-    def _setUpTreeView(self):
-        self.storemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
-        self.treeview.set_model(self.storemodel)
-
-        txtcell = gtk.CellRendererText()
-        txtcell.set_property("ellipsize", pango.ELLIPSIZE_START)
-        uricol = gtk.TreeViewColumn(_("File"), txtcell, text=0)
-        uricol.set_expand(True)
-        self.treeview.append_column(uricol)
-
-        txtcell2 = gtk.CellRendererText()
-        txtcell2.set_property("ellipsize", pango.ELLIPSIZE_END)
-        reasoncol = gtk.TreeViewColumn(_("Reason"), txtcell2, text=1)
-        reasoncol.set_expand(True)
-        self.treeview.append_column(reasoncol)
-
-    def addFailedFile(self, uri, reason=_("Unknown reason")):
-        """Add the given uri to the list of failed files. You can optionnaly
-        give a string identifying the reason why the file failed to be
-        discovered
-        """
-        gst.debug("Uri:%s, reason:%s" % (uri, reason))
-        self.storemodel.append([str(uri), str(reason)])
-        
-    def isVisible(self):
-        """ returns True if this dialog is currently shown """
-        return self.window.get_property("visible")
-
-    ## Callbacks from glade
-
-    def _closeCb(self, unused_dialog):
-        self.emit('close')
-
-    def _responseCb(self, unused_dialog, response):
-        self.emit('response', response)
diff -ru --exclude=CVS pitivi_pub/pitivi/ui/timelineobjects.py pitivi_rjb/pitivi/ui/timelineobjects.py
--- pitivi_pub/pitivi/ui/timelineobjects.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/ui/timelineobjects.py	2006-12-03 17:05:34.000000000 +0000
@@ -33,6 +33,7 @@
 from pitivi.timeline import Timeline, TimelineComposition, TimelineFileSource, TimelineSource, TimelineTransition, MEDIA_TYPE_AUDIO, MEDIA_TYPE_VIDEO
 import pitivi.dnd as dnd
 from sourcefactories import beautify_length
+from signalgroup import SignalGroup
 
 # Default width / height ratio for simple elements
 DEFAULT_SIMPLE_SIZE_RATIO = 1.0 # default width / height ratio
@@ -58,17 +59,14 @@
 
         self.hadjustment = self.get_property("hadjustment")
 
-        # timeline and top level compositions
-        self.timeline = instance.PiTiVi.current.timeline
-        self.condensed = self.timeline.videocomp.condensed
-
-        # TODO : connect signals for when the timeline changes
-
         # widgets correspondance dictionnary
         self.widgets = {}
 
-        self.timeline.videocomp.connect("condensed-list-changed",
-                                        self._condensedListChangedCb)
+        # Connect to project.  We must remove and reset the callbacks when
+        # changing project.
+        self.project_signals = SignalGroup()
+        self._connectToTimeline(instance.PiTiVi.current.timeline)
+        instance.PiTiVi.connect("new-project-loaded", self._newProjectCb)
 
         # size
         self.width = int(DEFAULT_WIDTH)
@@ -95,6 +93,22 @@
 
         self.show_all()
 
+    def _connectToTimeline(self, timeline):
+        self.timeline = timeline
+        self.condensed = self.timeline.videocomp.condensed
+
+        self.project_signals.connect(self.timeline.videocomp, "condensed-list-changed",
+                                     None, self._condensedListChangedCb)
+
+    def _newProjectCb(self, unused_pitivi, project):
+        assert(instance.PiTiVi.current == project)
+
+        for widget in self.widgets.itervalues():
+            self.remove(widget)
+        self.widgets = {}
+
+        self._connectToTimeline(instance.PiTiVi.current.timeline)
+
     def _condensedListChangedCb(self, unused_videocomp, clist):
         """ add/remove the widgets """
         gst.debug("condensed list changed in videocomp")
@@ -103,7 +117,7 @@
         # go through the condensed list
         for element in clist:
             if element in current:
-                # element stil exists
+                # element still exists
                 current.remove(element)
             else:
                 # new element
@@ -308,7 +322,6 @@
 
     def _sourceDeleteMeCb(self, widget, element):
         # remove this element from the timeline
-        print element
         self.timeline.videocomp.removeSource(element, collapse_neighbours=True)
 
 
diff -ru --exclude=CVS pitivi_pub/pitivi/ui/timeline.py pitivi_rjb/pitivi/ui/timeline.py
--- pitivi_pub/pitivi/ui/timeline.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/ui/timeline.py	2006-12-03 17:05:34.000000000 +0000
@@ -28,7 +28,7 @@
 import gobject
 import gst
 
-from timelineobjects import SimpleSourceWidget, SimpleTimeline
+from timelineobjects import SimpleTimeline
 from complextimeline import ComplexTimelineWidget
 
 class TimelineWidget(gtk.VBox):
diff -ru --exclude=CVS pitivi_pub/pitivi/ui/viewer.py pitivi_rjb/pitivi/ui/viewer.py
--- pitivi_pub/pitivi/ui/viewer.py	2006-12-03 18:50:42.000000000 +0000
+++ pitivi_rjb/pitivi/ui/viewer.py	2006-12-03 17:05:34.000000000 +0000
@@ -35,6 +35,7 @@
 import pitivi.dnd as dnd
 from pitivi.settings import ExportSettings
 from exportsettingswidget import ExportSettingsDialog
+from signalgroup import SignalGroup
 
 from gettext import gettext as _
 
@@ -62,21 +63,30 @@
         self.currentState = gst.STATE_PAUSED
         self._createUi()
 
-        # connect to the sourcelist for temp factories
-        # TODO remove/replace the signal when closing/opening projects
-        instance.PiTiVi.current.sources.connect("tmp_is_ready", self._tmpIsReadyCb)
+        # Connect to project.  We must remove and reset the callbacks when
+        # changing project.
+        self.project_signals = SignalGroup()
+        self._connectToProject(instance.PiTiVi.current)
+        instance.PiTiVi.connect("new-project-loaded", self._newProjectCb)
 
-        instance.PiTiVi.connect("new-project", self._newProjectCb)
         instance.PiTiVi.playground.connect("current-state", self._currentStateCb)
         instance.PiTiVi.playground.connect("position", self._playgroundPositionCb)
 
         # callback to know when to set the XID on our viewer widget
         instance.PiTiVi.playground.connect("element-message", self._playgroundElementMessageCb)
 
-        instance.PiTiVi.current.connect("settings-changed",
-                                        self._settingsChangedCb)
         self._addTimelineToPlayground()
 
+    def _connectToProject(self, project):
+        """Connect signal handlers to a project.
+ 
+        This first disconnects any handlers connected to an old project.
+        If project is None, this just disconnects any connected handlers.
+ 
+        """
+        self.project_signals.connect(project.sources, "tmp_is_ready", None, self._tmpIsReadyCb)
+        self.project_signals.connect(project, "settings-changed", None, self._settingsChangedCb)
+
     def _createUi(self):
         """ Creates the Viewer GUI """
         self.set_border_width(5)
@@ -345,10 +355,11 @@
         gst.info("%s" % factory)
         instance.PiTiVi.playground.playTemporaryFilesourcefactory(factory)
 
-    def _newProjectCb(self, unused_pitivi, unused_project):
+    def _newProjectCb(self, unused_pitivi, project):
         """ the current project has changed """
-        instance.PiTiVi.current.sources.connect("tmp_is_ready", self._tmpIsReadyCb)
-        instance.PiTiVi.current.connect("settings-changed", self._settingsChangedCb)
+        assert(instance.PiTiVi.current == project)
+        self._connectToProject(project)
+        instance.PiTiVi.playground.switchToDefault()
         
     def _addTimelineToPlayground(self):
         instance.PiTiVi.playground.addPipeline(instance.PiTiVi.current.getBin())
@@ -397,6 +408,9 @@
             self.slider.set_sensitive(False)
             self.playpause_button.set_sensitive(False)
             self.record_button.set_sensitive(False)
+            self._newTime(0)
+            instance.PiTiVi.playground.pause()
+            instance.PiTiVi.playground.play()
         else:
             if isinstance(smartbin, SmartTimelineBin):
                 gst.info("switching to Timeline, setting duration to %s" % (gst.TIME_ARGS(smartbin.project.timeline.videocomp.duration)))
