changeset 45:9ea770d93fae

* document -> framework (will not compile, path fixes not in yet)
author Chris Cannam
date Wed, 24 Oct 2007 16:37:58 +0000
parents 9ebe12983f3e
children 7fbe1c99d5d8
files framework/Document.cpp framework/Document.h framework/MainWindowBase.cpp framework/MainWindowBase.h framework/SVFileReader.cpp framework/SVFileReader.h framework/document.pro
diffstat 7 files changed, 5034 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/Document.cpp	Wed Oct 24 16:37:58 2007 +0000
@@ -0,0 +1,949 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "Document.h"
+
+#include "data/model/WaveFileModel.h"
+#include "data/model/WritableWaveFileModel.h"
+#include "data/model/DenseThreeDimensionalModel.h"
+#include "data/model/DenseTimeValueModel.h"
+#include "layer/Layer.h"
+#include "base/CommandHistory.h"
+#include "base/Command.h"
+#include "view/View.h"
+#include "base/PlayParameterRepository.h"
+#include "base/PlayParameters.h"
+#include "plugin/transform/TransformFactory.h"
+#include <QApplication>
+#include <QTextStream>
+#include <iostream>
+
+// For alignment:
+#include "data/model/AggregateWaveModel.h"
+#include "data/model/SparseTimeValueModel.h"
+#include "data/model/AlignmentModel.h"
+
+//!!! still need to handle command history, documentRestored/documentModified
+
+Document::Document() :
+    m_mainModel(0)
+{
+    connect(this, SIGNAL(modelAboutToBeDeleted(Model *)),
+            TransformFactory::getInstance(),
+            SLOT(modelAboutToBeDeleted(Model *)));
+}
+
+Document::~Document()
+{
+    //!!! Document should really own the command history.  atm we
+    //still refer to it in various places that don't have access to
+    //the document, be nice to fix that
+
+//    std::cerr << "\n\nDocument::~Document: about to clear command history" << std::endl;
+    CommandHistory::getInstance()->clear();
+    
+//    std::cerr << "Document::~Document: about to delete layers" << std::endl;
+    while (!m_layers.empty()) {
+	deleteLayer(*m_layers.begin(), true);
+    }
+
+    if (!m_models.empty()) {
+	std::cerr << "Document::~Document: WARNING: " 
+		  << m_models.size() << " model(s) still remain -- "
+		  << "should have been garbage collected when deleting layers"
+		  << std::endl;
+	while (!m_models.empty()) {
+            Model *model = m_models.begin()->first;
+	    if (model == m_mainModel) {
+		// just in case!
+		std::cerr << "Document::~Document: WARNING: Main model is also"
+			  << " in models list!" << std::endl;
+	    } else if (model) {
+		emit modelAboutToBeDeleted(model);
+                model->aboutToDelete();
+		delete model;
+	    }
+	    m_models.erase(m_models.begin());
+	}
+    }
+
+//    std::cerr << "Document::~Document: About to get rid of main model"
+//	      << std::endl;
+    if (m_mainModel) {
+        emit modelAboutToBeDeleted(m_mainModel);
+        m_mainModel->aboutToDelete();
+    }
+
+    emit mainModelChanged(0);
+    delete m_mainModel;
+
+}
+
+Layer *
+Document::createLayer(LayerFactory::LayerType type)
+{
+    Layer *newLayer = LayerFactory::getInstance()->createLayer(type);
+    if (!newLayer) return 0;
+
+    newLayer->setObjectName(getUniqueLayerName(newLayer->objectName()));
+
+    m_layers.insert(newLayer);
+    emit layerAdded(newLayer);
+
+    return newLayer;
+}
+
+Layer *
+Document::createMainModelLayer(LayerFactory::LayerType type)
+{
+    Layer *newLayer = createLayer(type);
+    if (!newLayer) return 0;
+    setModel(newLayer, m_mainModel);
+    return newLayer;
+}
+
+Layer *
+Document::createImportedLayer(Model *model)
+{
+    LayerFactory::LayerTypeSet types =
+	LayerFactory::getInstance()->getValidLayerTypes(model);
+
+    if (types.empty()) {
+	std::cerr << "WARNING: Document::importLayer: no valid display layer for model" << std::endl;
+	return 0;
+    }
+
+    //!!! for now, just use the first suitable layer type
+    LayerFactory::LayerType type = *types.begin();
+
+    Layer *newLayer = LayerFactory::getInstance()->createLayer(type);
+    if (!newLayer) return 0;
+
+    newLayer->setObjectName(getUniqueLayerName(newLayer->objectName()));
+
+    addImportedModel(model);
+    setModel(newLayer, model);
+
+    //!!! and all channels
+    setChannel(newLayer, -1);
+
+    m_layers.insert(newLayer);
+    emit layerAdded(newLayer);
+    return newLayer;
+}
+
+Layer *
+Document::createEmptyLayer(LayerFactory::LayerType type)
+{
+    Model *newModel =
+	LayerFactory::getInstance()->createEmptyModel(type, m_mainModel);
+    if (!newModel) return 0;
+
+    Layer *newLayer = createLayer(type);
+    if (!newLayer) {
+	delete newModel;
+	return 0;
+    }
+
+    addImportedModel(newModel);
+    setModel(newLayer, newModel);
+
+    return newLayer;
+}
+
+Layer *
+Document::createDerivedLayer(LayerFactory::LayerType type,
+			     TransformId transform)
+{
+    Layer *newLayer = createLayer(type);
+    if (!newLayer) return 0;
+
+    newLayer->setObjectName(getUniqueLayerName
+                            (TransformFactory::getInstance()->
+                             getTransformFriendlyName(transform)));
+
+    return newLayer;
+}
+
+Layer *
+Document::createDerivedLayer(TransformId transform,
+                             Model *inputModel, 
+                             const PluginTransform::ExecutionContext &context,
+                             QString configurationXml)
+{
+    Model *newModel = addDerivedModel(transform, inputModel,
+                                      context, configurationXml);
+    if (!newModel) {
+        // error already printed to stderr by addDerivedModel
+        emit modelGenerationFailed(transform);
+        return 0;
+    }
+
+    LayerFactory::LayerTypeSet types =
+	LayerFactory::getInstance()->getValidLayerTypes(newModel);
+
+    if (types.empty()) {
+	std::cerr << "WARNING: Document::createLayerForTransform: no valid display layer for output of transform " << transform.toStdString() << std::endl;
+	delete newModel;
+	return 0;
+    }
+
+    //!!! for now, just use the first suitable layer type
+
+    Layer *newLayer = createLayer(*types.begin());
+    setModel(newLayer, newModel);
+
+    //!!! We need to clone the model when adding the layer, so that it
+    //can be edited without affecting other layers that are based on
+    //the same model.  Unfortunately we can't just clone it now,
+    //because it probably hasn't been completed yet -- the transform
+    //runs in the background.  Maybe the transform has to handle
+    //cloning and cacheing models itself.
+    //
+    // Once we do clone models here, of course, we'll have to avoid
+    // leaking them too.
+    //
+    // We want the user to be able to add a model to a second layer
+    // _while it's still being calculated in the first_ and have it
+    // work quickly.  That means we need to put the same physical
+    // model pointer in both layers, so they can't actually be cloned.
+    
+    if (newLayer) {
+	newLayer->setObjectName(getUniqueLayerName
+                                (TransformFactory::getInstance()->
+                                 getTransformFriendlyName(transform)));
+    }
+
+    emit layerAdded(newLayer);
+    return newLayer;
+}
+
+void
+Document::setMainModel(WaveFileModel *model)
+{
+    Model *oldMainModel = m_mainModel;
+    m_mainModel = model;
+
+    emit modelAdded(m_mainModel);
+
+    std::vector<Layer *> obsoleteLayers;
+    std::set<QString> failedTransforms;
+
+    // We need to ensure that no layer is left using oldMainModel or
+    // any of the old derived models as its model.  Either replace the
+    // model, or delete the layer for each layer that is currently
+    // using one of these.  Carry out this replacement before we
+    // delete any of the models.
+
+    for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+
+	Layer *layer = *i;
+	Model *model = layer->getModel();
+
+//        std::cerr << "Document::setMainModel: inspecting model "
+//                  << (model ? model->objectName().toStdString() : "(null)") << " in layer "
+//                  << layer->objectName().toStdString() << std::endl;
+
+	if (model == oldMainModel) {
+//            std::cerr << "... it uses the old main model, replacing" << std::endl;
+	    LayerFactory::getInstance()->setModel(layer, m_mainModel);
+	    continue;
+	}
+
+	if (m_models.find(model) == m_models.end()) {
+	    std::cerr << "WARNING: Document::setMainModel: Unknown model "
+		      << model << " in layer " << layer << std::endl;
+	    // get rid of this hideous degenerate
+	    obsoleteLayers.push_back(layer);
+	    continue;
+	}
+	    
+	if (m_models[model].source == oldMainModel) {
+
+//            std::cerr << "... it uses a model derived from the old main model, regenerating" << std::endl;
+
+	    // This model was derived from the previous main
+	    // model: regenerate it.
+	    
+	    TransformId transform = m_models[model].transform;
+            PluginTransform::ExecutionContext context = m_models[model].context;
+	    
+	    Model *replacementModel =
+                addDerivedModel(transform,
+                                m_mainModel,
+                                context,
+                                m_models[model].configurationXml);
+	    
+	    if (!replacementModel) {
+		std::cerr << "WARNING: Document::setMainModel: Failed to regenerate model for transform \""
+			  << transform.toStdString() << "\"" << " in layer " << layer << std::endl;
+                if (failedTransforms.find(transform) == failedTransforms.end()) {
+                    emit modelRegenerationFailed(layer->objectName(),
+                                                 transform);
+                    failedTransforms.insert(transform);
+                }
+		obsoleteLayers.push_back(layer);
+	    } else {
+                std::cerr << "Replacing model " << model << " (type "
+                          << typeid(*model).name() << ") with model "
+                          << replacementModel << " (type "
+                          << typeid(*replacementModel).name() << ") in layer "
+                          << layer << " (name " << layer->objectName().toStdString() << ")"
+                          << std::endl;
+                RangeSummarisableTimeValueModel *rm =
+                    dynamic_cast<RangeSummarisableTimeValueModel *>(replacementModel);
+                if (rm) {
+                    std::cerr << "new model has " << rm->getChannelCount() << " channels " << std::endl;
+                } else {
+                    std::cerr << "new model is not a RangeSummarisableTimeValueModel!" << std::endl;
+                }
+		setModel(layer, replacementModel);
+	    }
+	}	    
+    }
+
+    for (size_t k = 0; k < obsoleteLayers.size(); ++k) {
+	deleteLayer(obsoleteLayers[k], true);
+    }
+
+    emit mainModelChanged(m_mainModel);
+
+    // we already emitted modelAboutToBeDeleted for this
+    delete oldMainModel;
+}
+
+void
+Document::addDerivedModel(TransformId transform,
+                          Model *inputModel,
+                          const PluginTransform::ExecutionContext &context,
+                          Model *outputModelToAdd,
+                          QString configurationXml)
+{
+    if (m_models.find(outputModelToAdd) != m_models.end()) {
+	std::cerr << "WARNING: Document::addDerivedModel: Model already added"
+		  << std::endl;
+	return;
+    }
+
+//    std::cerr << "Document::addDerivedModel: source is " << inputModel << " \"" << inputModel->objectName().toStdString() << "\"" << std::endl;
+
+    ModelRecord rec;
+    rec.source = inputModel;
+    rec.transform = transform;
+    rec.context = context;
+    rec.configurationXml = configurationXml;
+    rec.refcount = 0;
+
+    outputModelToAdd->setSourceModel(inputModel);
+
+    m_models[outputModelToAdd] = rec;
+
+    emit modelAdded(outputModelToAdd);
+}
+
+
+void
+Document::addImportedModel(Model *model)
+{
+    if (m_models.find(model) != m_models.end()) {
+	std::cerr << "WARNING: Document::addImportedModel: Model already added"
+		  << std::endl;
+	return;
+    }
+
+    ModelRecord rec;
+    rec.source = 0;
+    rec.transform = "";
+    rec.refcount = 0;
+
+    m_models[model] = rec;
+
+    emit modelAdded(model);
+}
+
+Model *
+Document::addDerivedModel(TransformId transform,
+                          Model *inputModel,
+                          const PluginTransform::ExecutionContext &context,
+                          QString configurationXml)
+{
+    Model *model = 0;
+
+    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+	if (i->second.transform == transform &&
+	    i->second.source == inputModel && 
+            i->second.context == context &&
+            i->second.configurationXml == configurationXml) {
+	    return i->first;
+	}
+    }
+
+    model = TransformFactory::getInstance()->transform
+	(transform, inputModel, context, configurationXml);
+
+    if (!model) {
+	std::cerr << "WARNING: Document::addDerivedModel: no output model for transform " << transform.toStdString() << std::endl;
+    } else {
+	addDerivedModel(transform, inputModel, context, model, configurationXml);
+    }
+
+    return model;
+}
+
+void
+Document::releaseModel(Model *model) // Will _not_ release main model!
+{
+    if (model == 0) {
+	return;
+    }
+
+    if (model == m_mainModel) {
+	return;
+    }
+
+    bool toDelete = false;
+
+    if (m_models.find(model) != m_models.end()) {
+	
+	if (m_models[model].refcount == 0) {
+	    std::cerr << "WARNING: Document::releaseModel: model " << model
+		      << " reference count is zero already!" << std::endl;
+	} else {
+	    if (--m_models[model].refcount == 0) {
+		toDelete = true;
+	    }
+	}
+    } else { 
+	std::cerr << "WARNING: Document::releaseModel: Unfound model "
+		  << model << std::endl;
+	toDelete = true;
+    }
+
+    if (toDelete) {
+
+	int sourceCount = 0;
+
+	for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+	    if (i->second.source == model) {
+		++sourceCount;
+		i->second.source = 0;
+	    }
+	}
+
+	if (sourceCount > 0) {
+	    std::cerr << "Document::releaseModel: Deleting model "
+		      << model << " even though it is source for "
+		      << sourceCount << " other derived model(s) -- resetting "
+		      << "their source fields appropriately" << std::endl;
+	}
+
+	emit modelAboutToBeDeleted(model);
+        model->aboutToDelete();
+	m_models.erase(model);
+	delete model;
+    }
+}
+
+void
+Document::deleteLayer(Layer *layer, bool force)
+{
+    if (m_layerViewMap.find(layer) != m_layerViewMap.end() &&
+	m_layerViewMap[layer].size() > 0) {
+
+	std::cerr << "WARNING: Document::deleteLayer: Layer "
+		  << layer << " [" << layer->objectName().toStdString() << "]"
+		  << " is still used in " << m_layerViewMap[layer].size()
+		  << " views!" << std::endl;
+
+	if (force) {
+
+	    std::cerr << "(force flag set -- deleting from all views)" << std::endl;
+
+	    for (std::set<View *>::iterator j = m_layerViewMap[layer].begin();
+		 j != m_layerViewMap[layer].end(); ++j) {
+		// don't use removeLayerFromView, as it issues a command
+		layer->setLayerDormant(*j, true);
+		(*j)->removeLayer(layer);
+	    }
+	    
+	    m_layerViewMap.erase(layer);
+
+	} else {
+	    return;
+	}
+    }
+
+    if (m_layers.find(layer) == m_layers.end()) {
+	std::cerr << "Document::deleteLayer: Layer "
+		  << layer << " does not exist, or has already been deleted "
+		  << "(this may not be as serious as it sounds)" << std::endl;
+	return;
+    }
+
+    m_layers.erase(layer);
+
+    releaseModel(layer->getModel());
+    emit layerRemoved(layer);
+    emit layerAboutToBeDeleted(layer);
+    delete layer;
+}
+
+void
+Document::setModel(Layer *layer, Model *model)
+{
+    if (model && 
+	model != m_mainModel &&
+	m_models.find(model) == m_models.end()) {
+	std::cerr << "ERROR: Document::setModel: Layer " << layer
+		  << " (\"" << layer->objectName().toStdString()
+                  << "\") wants to use unregistered model " << model
+		  << ": register the layer's model before setting it!"
+		  << std::endl;
+	return;
+    }
+
+    Model *previousModel = layer->getModel();
+
+    if (previousModel == model) {
+        std::cerr << "WARNING: Document::setModel: Layer " << layer << " (\""
+                  << layer->objectName().toStdString()
+                  << "\") is already set to model "
+                  << model << " (\""
+                  << (model ? model->objectName().toStdString() : "(null)")
+                  << "\")" << std::endl;
+        return;
+    }
+
+    if (model && model != m_mainModel) {
+	m_models[model].refcount ++;
+    }
+
+    if (model && previousModel) {
+        PlayParameterRepository::getInstance()->copyParameters
+            (previousModel, model);
+    }
+
+    LayerFactory::getInstance()->setModel(layer, model);
+
+    if (previousModel) {
+        releaseModel(previousModel);
+    }
+}
+
+void
+Document::setChannel(Layer *layer, int channel)
+{
+    LayerFactory::getInstance()->setChannel(layer, channel);
+}
+
+void
+Document::addLayerToView(View *view, Layer *layer)
+{
+    Model *model = layer->getModel();
+    if (!model) {
+//	std::cerr << "Document::addLayerToView: Layer (\""
+//                  << layer->objectName().toStdString()
+//                  << "\") with no model being added to view: "
+//                  << "normally you want to set the model first" << std::endl;
+    } else {
+	if (model != m_mainModel &&
+	    m_models.find(model) == m_models.end()) {
+	    std::cerr << "ERROR: Document::addLayerToView: Layer " << layer
+		      << " has unregistered model " << model
+		      << " -- register the layer's model before adding the layer!" << std::endl;
+	    return;
+	}
+    }
+
+    CommandHistory::getInstance()->addCommand
+	(new Document::AddLayerCommand(this, view, layer));
+}
+
+void
+Document::removeLayerFromView(View *view, Layer *layer)
+{
+    CommandHistory::getInstance()->addCommand
+	(new Document::RemoveLayerCommand(this, view, layer));
+}
+
+void
+Document::addToLayerViewMap(Layer *layer, View *view)
+{
+    bool firstView = (m_layerViewMap.find(layer) == m_layerViewMap.end() ||
+                      m_layerViewMap[layer].empty());
+
+    if (m_layerViewMap[layer].find(view) !=
+	m_layerViewMap[layer].end()) {
+	std::cerr << "WARNING: Document::addToLayerViewMap:"
+		  << " Layer " << layer << " -> view " << view << " already in"
+		  << " layer view map -- internal inconsistency" << std::endl;
+    }
+
+    m_layerViewMap[layer].insert(view);
+
+    if (firstView) emit layerInAView(layer, true);
+}
+    
+void
+Document::removeFromLayerViewMap(Layer *layer, View *view)
+{
+    if (m_layerViewMap[layer].find(view) ==
+	m_layerViewMap[layer].end()) {
+	std::cerr << "WARNING: Document::removeFromLayerViewMap:"
+		  << " Layer " << layer << " -> view " << view << " not in"
+		  << " layer view map -- internal inconsistency" << std::endl;
+    }
+
+    m_layerViewMap[layer].erase(view);
+
+    if (m_layerViewMap[layer].empty()) {
+        m_layerViewMap.erase(layer);
+        emit layerInAView(layer, false);
+    }
+}
+
+QString
+Document::getUniqueLayerName(QString candidate)
+{
+    for (int count = 1; ; ++count) {
+
+        QString adjusted =
+            (count > 1 ? QString("%1 <%2>").arg(candidate).arg(count) :
+             candidate);
+        
+        bool duplicate = false;
+
+        for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+            if ((*i)->objectName() == adjusted) {
+                duplicate = true;
+                break;
+            }
+        }
+
+        if (!duplicate) return adjusted;
+    }
+}
+
+std::vector<Model *>
+Document::getTransformInputModels()
+{
+    std::vector<Model *> models;
+
+    if (!m_mainModel) return models;
+
+    models.push_back(m_mainModel);
+
+    //!!! This will pick up all models, including those that aren't visible...
+
+    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+
+        Model *model = i->first;
+        if (!model || model == m_mainModel) continue;
+        DenseTimeValueModel *dtvm = dynamic_cast<DenseTimeValueModel *>(model);
+        
+        if (dtvm) {
+            models.push_back(dtvm);
+        }
+    }
+
+    return models;
+}
+
+void
+Document::alignModel(Model *model)
+{
+    if (!m_mainModel || model == m_mainModel) return;
+
+    RangeSummarisableTimeValueModel *rm = 
+        dynamic_cast<RangeSummarisableTimeValueModel *>(model);
+    if (!rm) return;
+    
+    // This involves creating three new models:
+
+    // 1. an AggregateWaveModel to provide the mixdowns of the main
+    // model and the new model in its two channels, as input to the
+    // MATCH plugin
+
+    // 2. a SparseTimeValueModel, which is the model automatically
+    // created by FeatureExtractionPluginTransform when running the
+    // MATCH plugin (thus containing the alignment path)
+
+    // 3. an AlignmentModel, which stores the path model and carries
+    // out alignment lookups on it.
+
+    // The first two of these are provided as arguments to the
+    // constructor for the third, which takes responsibility for
+    // deleting them.  The AlignmentModel, meanwhile, is passed to the
+    // new model we are aligning, which also takes responsibility for
+    // it.  We should not have to delete any of these new models here.
+
+    AggregateWaveModel::ChannelSpecList components;
+
+    components.push_back(AggregateWaveModel::ModelChannelSpec
+                         (m_mainModel, -1));
+
+    components.push_back(AggregateWaveModel::ModelChannelSpec
+                         (rm, -1));
+
+    Model *aggregate = new AggregateWaveModel(components);
+
+    TransformId id = "vamp:match-vamp-plugin:match:path";
+    
+    TransformFactory *factory = TransformFactory::getInstance();
+
+    Model *transformOutput = factory->transform
+        (id, aggregate,
+         factory->getDefaultContextForTransform(id, aggregate),
+         "<plugin param-serialise=\"1\"/>");
+
+    SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *>
+        (transformOutput);
+
+    if (!path) {
+        std::cerr << "Document::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << std::endl;
+        delete transformOutput;
+        delete aggregate;
+        return;
+    }
+
+    AlignmentModel *alignmentModel = new AlignmentModel
+        (m_mainModel, model, aggregate, path);
+
+    rm->setAlignment(alignmentModel);
+}
+
+void
+Document::alignModels()
+{
+    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+        alignModel(i->first);
+    }
+}
+
+Document::AddLayerCommand::AddLayerCommand(Document *d,
+					   View *view,
+					   Layer *layer) :
+    m_d(d),
+    m_view(view),
+    m_layer(layer),
+    m_name(qApp->translate("AddLayerCommand", "Add %1 Layer").arg(layer->objectName())),
+    m_added(false)
+{
+}
+
+Document::AddLayerCommand::~AddLayerCommand()
+{
+//    std::cerr << "Document::AddLayerCommand::~AddLayerCommand" << std::endl;
+    if (!m_added) {
+	m_d->deleteLayer(m_layer);
+    }
+}
+
+void
+Document::AddLayerCommand::execute()
+{
+    for (int i = 0; i < m_view->getLayerCount(); ++i) {
+	if (m_view->getLayer(i) == m_layer) {
+	    // already there
+	    m_layer->setLayerDormant(m_view, false);
+	    m_added = true;
+	    return;
+	}
+    }
+
+    m_view->addLayer(m_layer);
+    m_layer->setLayerDormant(m_view, false);
+
+    m_d->addToLayerViewMap(m_layer, m_view);
+    m_added = true;
+}
+
+void
+Document::AddLayerCommand::unexecute()
+{
+    m_view->removeLayer(m_layer);
+    m_layer->setLayerDormant(m_view, true);
+
+    m_d->removeFromLayerViewMap(m_layer, m_view);
+    m_added = false;
+}
+
+Document::RemoveLayerCommand::RemoveLayerCommand(Document *d,
+						 View *view,
+						 Layer *layer) :
+    m_d(d),
+    m_view(view),
+    m_layer(layer),
+    m_name(qApp->translate("RemoveLayerCommand", "Delete %1 Layer").arg(layer->objectName())),
+    m_added(true)
+{
+}
+
+Document::RemoveLayerCommand::~RemoveLayerCommand()
+{
+//    std::cerr << "Document::RemoveLayerCommand::~RemoveLayerCommand" << std::endl;
+    if (!m_added) {
+	m_d->deleteLayer(m_layer);
+    }
+}
+
+void
+Document::RemoveLayerCommand::execute()
+{
+    bool have = false;
+    for (int i = 0; i < m_view->getLayerCount(); ++i) {
+	if (m_view->getLayer(i) == m_layer) {
+	    have = true;
+	    break;
+	}
+    }
+
+    if (!have) { // not there!
+	m_layer->setLayerDormant(m_view, true);
+	m_added = false;
+	return;
+    }
+
+    m_view->removeLayer(m_layer);
+    m_layer->setLayerDormant(m_view, true);
+
+    m_d->removeFromLayerViewMap(m_layer, m_view);
+    m_added = false;
+}
+
+void
+Document::RemoveLayerCommand::unexecute()
+{
+    m_view->addLayer(m_layer);
+    m_layer->setLayerDormant(m_view, false);
+
+    m_d->addToLayerViewMap(m_layer, m_view);
+    m_added = true;
+}
+
+void
+Document::toXml(QTextStream &out, QString indent, QString extraAttributes) const
+{
+    out << indent + QString("<data%1%2>\n")
+        .arg(extraAttributes == "" ? "" : " ").arg(extraAttributes);
+
+    if (m_mainModel) {
+	m_mainModel->toXml(out, indent + "  ", "mainModel=\"true\"");
+    }
+
+    // Models that are not used in a layer that is in a view should
+    // not be written.  Get our list of required models first.
+
+    std::set<const Model *> used;
+
+    for (LayerViewMap::const_iterator i = m_layerViewMap.begin();
+         i != m_layerViewMap.end(); ++i) {
+
+        if (i->first && !i->second.empty() && i->first->getModel()) {
+            used.insert(i->first->getModel());
+        }
+    }
+
+    for (ModelMap::const_iterator i = m_models.begin();
+	 i != m_models.end(); ++i) {
+
+        const Model *model = i->first;
+	const ModelRecord &rec = i->second;
+
+        if (used.find(model) == used.end()) continue;
+        
+        // We need an intelligent way to determine which models need
+        // to be streamed (i.e. have been edited, or are small) and
+        // which should not be (i.e. remain as generated by a
+        // transform, and are large).
+        //
+        // At the moment we can get away with deciding not to stream
+        // dense 3d models or writable wave file models, provided they
+        // were generated from a transform, because at the moment there
+        // is no way to edit those model types so it should be safe to
+        // regenerate them.  That won't always work in future though.
+        // It would be particularly nice to be able to ask the user,
+        // as well as making an intelligent guess.
+
+        bool writeModel = true;
+        bool haveDerivation = false;
+
+        if (rec.source && rec.transform != "") {
+            haveDerivation = true;
+        } 
+
+        if (haveDerivation) {
+            if (dynamic_cast<const WritableWaveFileModel *>(model)) {
+                writeModel = false;
+            } else if (dynamic_cast<const DenseThreeDimensionalModel *>(model)) {
+                writeModel = false;
+            }
+        }
+
+        if (writeModel) {
+            i->first->toXml(out, indent + "  ");
+        }
+
+	if (haveDerivation) {
+
+            QString extentsAttributes;
+            if (rec.context.startFrame != 0 ||
+                rec.context.duration != 0) {
+                extentsAttributes = QString("startFrame=\"%1\" duration=\"%2\" ")
+                    .arg(rec.context.startFrame)
+                    .arg(rec.context.duration);
+            }
+	    
+	    out << indent;
+	    out << QString("  <derivation source=\"%1\" model=\"%2\" channel=\"%3\" domain=\"%4\" stepSize=\"%5\" blockSize=\"%6\" %7windowType=\"%8\" transform=\"%9\"")
+		.arg(XmlExportable::getObjectExportId(rec.source))
+		.arg(XmlExportable::getObjectExportId(i->first))
+                .arg(rec.context.channel)
+                .arg(rec.context.domain)
+                .arg(rec.context.stepSize)
+                .arg(rec.context.blockSize)
+                .arg(extentsAttributes)
+                .arg(int(rec.context.windowType))
+		.arg(XmlExportable::encodeEntities(rec.transform));
+
+            if (rec.configurationXml != "") {
+                out << ">\n    " + indent + rec.configurationXml
+                    + "\n" + indent + "  </derivation>\n";
+            } else {
+                out << "/>\n";
+            }
+	}
+
+        //!!! We should probably own the PlayParameterRepository
+        PlayParameters *playParameters =
+            PlayParameterRepository::getInstance()->getPlayParameters(i->first);
+        if (playParameters) {
+            playParameters->toXml
+                (out, indent + "  ",
+                 QString("model=\"%1\"")
+                 .arg(XmlExportable::getObjectExportId(i->first)));
+        }
+    }
+	    
+    for (LayerSet::const_iterator i = m_layers.begin();
+	 i != m_layers.end(); ++i) {
+
+	(*i)->toXml(out, indent + "  ");
+    }
+
+    out << indent + "</data>\n";
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/Document.h	Wed Oct 24 16:37:58 2007 +0000
@@ -0,0 +1,316 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _DOCUMENT_H_
+#define _DOCUMENT_H_
+
+#include "layer/LayerFactory.h"
+#include "plugin/transform/Transform.h"
+#include "plugin/transform/PluginTransform.h"
+#include "base/Command.h"
+
+#include <map>
+#include <set>
+
+class Model;
+class Layer;
+class View;
+class WaveFileModel;
+
+/**
+ * A Sonic Visualiser document consists of a set of data models, and
+ * also the visualisation layers used to display them.  Changes to the
+ * layers and their layout need to be stored and managed in much the
+ * same way as changes to the underlying data.
+ * 
+ * The document manages:
+ * 
+ *  - A main data Model, which provides the underlying sample rate and
+ * such like.  This must be a WaveFileModel.
+ * 
+ *  - Any number of imported Model objects, which contain data without any
+ * requirement to remember where the data came from or how to
+ * regenerate it.
+ * 
+ *  - Any number of Model objects that were generated by a Transform
+ * such as FeatureExtractionPluginTransform.  For these, we also
+ * record the source model and the name of the transform used to
+ * generate the model so that we can regenerate it (potentially
+ * from a different source) on demand.
+ *
+ *  - A flat list of Layer objects.  Elsewhere, the GUI may distribute these
+ * across any number of View widgets.  A layer may be viewable on more
+ * than one view at once, in principle.  A layer refers to one model,
+ * but the same model can be in use in more than one layer.
+ *
+ * The document does *not* manage the existence or structure of Pane
+ * and other view widgets.  However, it does provide convenience
+ * methods for reference-counted command-based management of the
+ * association between layers and views (addLayerToView,
+ * removeLayerFromView).
+ */
+
+class Document : public QObject,
+		 public XmlExportable
+{
+    Q_OBJECT
+
+public:
+    Document();
+    virtual ~Document();
+
+    /**
+     * Create and return a new layer of the given type, associated
+     * with no model.  The caller may set any model on this layer, but
+     * the model must also be registered with the document via the
+     * add-model methods below.
+     */
+    Layer *createLayer(LayerFactory::LayerType);
+
+    /**
+     * Create and return a new layer of the given type, associated
+     * with the current main model (if appropriate to the layer type).
+     */
+    Layer *createMainModelLayer(LayerFactory::LayerType);
+
+    /**
+     * Create and return a new layer associated with the given model,
+     * and register the model as an imported model.
+     */
+    Layer *createImportedLayer(Model *);
+
+    /**
+     * Create and return a new layer of the given type, with an
+     * appropriate empty model.  If the given type is not one for
+     * which an empty model can meaningfully be created, return 0.
+     */
+    Layer *createEmptyLayer(LayerFactory::LayerType);
+
+    /**
+     * Create and return a new layer of the given type, associated
+     * with the given transform name.  This method does not run the
+     * transform itself, nor create a model.  The caller can safely
+     * add a model to the layer later, but note that all models used
+     * by a transform layer _must_ be registered with the document
+     * using addDerivedModel below.
+     */
+    Layer *createDerivedLayer(LayerFactory::LayerType, TransformId);
+
+    /**
+     * Create and return a suitable layer for the given transform,
+     * running the transform and associating the resulting model with
+     * the new layer.
+     */
+    Layer *createDerivedLayer(TransformId,
+                              Model *inputModel, 
+                              const PluginTransform::ExecutionContext &context,
+                              QString configurationXml);
+
+    /**
+     * Set the main model (the source for playback sample rate, etc)
+     * to the given wave file model.  This will regenerate any derived
+     * models that were based on the previous main model.
+     */
+    void setMainModel(WaveFileModel *);
+
+    /**
+     * Get the main model (the source for playback sample rate, etc).
+     */
+    WaveFileModel *getMainModel() { return m_mainModel; }
+
+    /**
+     * Get the main model (the source for playback sample rate, etc).
+     */
+    const WaveFileModel *getMainModel() const { return m_mainModel; }
+
+    std::vector<Model *> getTransformInputModels();
+
+    /**
+     * Add a derived model associated with the given transform,
+     * running the transform and returning the resulting model.
+     */
+    Model *addDerivedModel(TransformId transform,
+                           Model *inputModel,
+                           const PluginTransform::ExecutionContext &context,
+                           QString configurationXml);
+
+    /**
+     * Add a derived model associated with the given transform.  This
+     * is necessary to register any derived model that was not created
+     * by the document using createDerivedModel or createDerivedLayer.
+     */
+    void addDerivedModel(TransformId,
+                         Model *inputModel,
+                         const PluginTransform::ExecutionContext &context,
+                         Model *outputModelToAdd,
+                         QString configurationXml);
+
+    /**
+     * Add an imported (non-derived, non-main) model.  This is
+     * necessary to register any imported model that is associated
+     * with a layer.
+     */
+    void addImportedModel(Model *);
+
+    /**
+     * Associate the given model with the given layer.  The model must
+     * have already been registered using one of the addXXModel
+     * methods above.
+     */
+    void setModel(Layer *, Model *);
+
+    /**
+     * Set the given layer to use the given channel of its model (-1
+     * means all available channels).
+     */
+    void setChannel(Layer *, int);
+
+    /**
+     * Add the given layer to the given view.  If the layer is
+     * intended to show a particular model, the model should normally
+     * be set using setModel before this method is called.
+     */
+    void addLayerToView(View *, Layer *);
+
+    /**
+     * Remove the given layer from the given view.
+     */
+    void removeLayerFromView(View *, Layer *);
+
+    void toXml(QTextStream &, QString indent, QString extraAttributes) const;
+signals:
+    void layerAdded(Layer *);
+    void layerRemoved(Layer *);
+    void layerAboutToBeDeleted(Layer *);
+
+    // Emitted when a layer is first added to a view, or when it is
+    // last removed from a view
+    void layerInAView(Layer *, bool);
+
+    void modelAdded(Model *);
+    void mainModelChanged(WaveFileModel *); // emitted after modelAdded
+    void modelAboutToBeDeleted(Model *);
+
+    void modelGenerationFailed(QString transformName);
+    void modelRegenerationFailed(QString layerName, QString transformName);
+
+protected:
+    void releaseModel(Model *model);
+
+    /**
+     * Delete the given layer, and also its associated model if no
+     * longer used by any other layer.  In general, this should be the
+     * only method used to delete layers -- doing so directly is a bit
+     * of a social gaffe.
+     */
+    void deleteLayer(Layer *, bool force = false);
+
+    /**
+     * If model is suitable for alignment, align it against the main
+     * model and store the alignment in the model.
+     */
+    void alignModel(Model *);
+
+    /**
+     * Realign all models if the main model has changed.  Is this wise?
+     */
+    void alignModels();
+
+    /*
+     * Every model that is in use by a layer in the document must be
+     * found in either m_mainModel or m_models.  We own and control
+     * the lifespan of all of these models.
+     */
+
+    /**
+     * The model that provides the underlying sample rate, etc.  This
+     * model is not reference counted for layers, and is not freed
+     * unless it is replaced or the document is deleted.
+     */
+    WaveFileModel *m_mainModel;
+
+    struct ModelRecord
+    {
+	// Information associated with a non-main model.  If this
+	// model is derived from another, then source will be non-NULL
+	// and the transform name will be set appropriately.  If the
+	// transform name is set but source is NULL, then there was a
+	// transform involved but the (target) model has been modified
+	// since being generated from it.
+	const Model *source;
+	TransformId transform;
+        PluginTransform::ExecutionContext context;
+        QString configurationXml;
+
+	// Count of the number of layers using this model.
+	int refcount;
+    };
+
+    typedef std::map<Model *, ModelRecord> ModelMap;
+    ModelMap m_models;
+
+    class AddLayerCommand : public Command
+    {
+    public:
+	AddLayerCommand(Document *d, View *view, Layer *layer);
+	virtual ~AddLayerCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const { return m_name; }
+
+    protected:
+	Document *m_d;
+	View *m_view; // I don't own this
+	Layer *m_layer; // Document owns this, but I determine its lifespans
+	QString m_name;
+	bool m_added;
+    };
+
+    class RemoveLayerCommand : public Command
+    {
+    public:
+	RemoveLayerCommand(Document *d, View *view, Layer *layer);
+	virtual ~RemoveLayerCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const { return m_name; }
+
+    protected:
+	Document *m_d;
+	View *m_view; // I don't own this
+	Layer *m_layer; // Document owns this, but I determine its lifespan
+	QString m_name;
+	bool m_added;
+    };
+
+    typedef std::map<Layer *, std::set<View *> > LayerViewMap;
+    LayerViewMap m_layerViewMap;
+
+    void addToLayerViewMap(Layer *, View *);
+    void removeFromLayerViewMap(Layer *, View *);
+
+    QString getUniqueLayerName(QString candidate);
+    
+    /**
+     * And these are the layers.  We also control the lifespans of
+     * these (usually through the commands used to add and remove them).
+     */
+    typedef std::set<Layer *> LayerSet;
+    LayerSet m_layers;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/MainWindowBase.cpp	Wed Oct 24 16:37:58 2007 +0000
@@ -0,0 +1,2006 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "MainWindowBase.h"
+#include "document/Document.h"
+
+
+#include "view/Pane.h"
+#include "view/PaneStack.h"
+#include "data/model/WaveFileModel.h"
+#include "data/model/SparseOneDimensionalModel.h"
+#include "data/model/NoteModel.h"
+#include "data/model/Labeller.h"
+#include "view/ViewManager.h"
+
+#include "layer/WaveformLayer.h"
+#include "layer/TimeRulerLayer.h"
+#include "layer/TimeInstantLayer.h"
+#include "layer/TimeValueLayer.h"
+#include "layer/Colour3DPlotLayer.h"
+#include "layer/SliceLayer.h"
+#include "layer/SliceableLayer.h"
+#include "layer/ImageLayer.h"
+
+#include "widgets/ListInputDialog.h"
+
+#include "audioio/AudioCallbackPlaySource.h"
+#include "audioio/AudioCallbackPlayTarget.h"
+#include "audioio/AudioTargetFactory.h"
+#include "audioio/PlaySpeedRangeMapper.h"
+#include "data/fileio/DataFileReaderFactory.h"
+#include "data/fileio/PlaylistFileReader.h"
+#include "data/fileio/WavFileWriter.h"
+#include "data/fileio/CSVFileWriter.h"
+#include "data/fileio/MIDIFileWriter.h"
+#include "data/fileio/BZipFileDevice.h"
+#include "data/fileio/FileSource.h"
+
+#include "data/fft/FFTDataServer.h"
+
+#include "base/RecentFiles.h"
+
+#include "base/PlayParameterRepository.h"
+#include "base/XmlExportable.h"
+#include "base/CommandHistory.h"
+#include "base/Profiler.h"
+#include "base/Preferences.h"
+
+#include "data/osc/OSCQueue.h"
+
+#include <QApplication>
+#include <QMessageBox>
+#include <QGridLayout>
+#include <QLabel>
+#include <QAction>
+#include <QMenuBar>
+#include <QToolBar>
+#include <QInputDialog>
+#include <QStatusBar>
+#include <QTreeView>
+#include <QFile>
+#include <QFileInfo>
+#include <QDir>
+#include <QTextStream>
+#include <QProcess>
+#include <QShortcut>
+#include <QSettings>
+#include <QDateTime>
+#include <QProcess>
+#include <QCheckBox>
+#include <QRegExp>
+#include <QScrollArea>
+
+#include <iostream>
+#include <cstdio>
+#include <errno.h>
+
+using std::cerr;
+using std::endl;
+
+using std::vector;
+using std::map;
+using std::set;
+
+
+MainWindowBase::MainWindowBase(bool withAudioOutput, bool withOSCSupport) :
+    m_document(0),
+    m_paneStack(0),
+    m_viewManager(0),
+    m_timeRulerLayer(0),
+    m_audioOutput(withAudioOutput),
+    m_playSource(0),
+    m_playTarget(0),
+    m_oscQueue(withOSCSupport ? new OSCQueue() : 0),
+    m_recentFiles("RecentFiles", 20),
+    m_recentTransforms("RecentTransforms", 20),
+    m_documentModified(false),
+    m_openingAudioFile(false),
+    m_abandoning(false),
+    m_labeller(0)
+{
+    connect(CommandHistory::getInstance(), SIGNAL(commandExecuted()),
+	    this, SLOT(documentModified()));
+    connect(CommandHistory::getInstance(), SIGNAL(documentRestored()),
+	    this, SLOT(documentRestored()));
+    
+    m_viewManager = new ViewManager();
+    connect(m_viewManager, SIGNAL(selectionChanged()),
+	    this, SLOT(updateMenuStates()));
+    connect(m_viewManager, SIGNAL(inProgressSelectionChanged()),
+	    this, SLOT(inProgressSelectionChanged()));
+
+    Preferences::BackgroundMode mode =
+        Preferences::getInstance()->getBackgroundMode();
+    m_initialDarkBackground = m_viewManager->getGlobalDarkBackground();
+    if (mode != Preferences::BackgroundFromTheme) {
+        m_viewManager->setGlobalDarkBackground
+            (mode == Preferences::DarkBackground);
+    }
+
+    m_paneStack = new PaneStack(0, m_viewManager);
+    connect(m_paneStack, SIGNAL(currentPaneChanged(Pane *)),
+	    this, SLOT(currentPaneChanged(Pane *)));
+    connect(m_paneStack, SIGNAL(currentLayerChanged(Pane *, Layer *)),
+	    this, SLOT(currentLayerChanged(Pane *, Layer *)));
+    connect(m_paneStack, SIGNAL(rightButtonMenuRequested(Pane *, QPoint)),
+            this, SLOT(rightButtonMenuRequested(Pane *, QPoint)));
+    connect(m_paneStack, SIGNAL(contextHelpChanged(const QString &)),
+            this, SLOT(contextHelpChanged(const QString &)));
+    connect(m_paneStack, SIGNAL(paneAdded(Pane *)),
+            this, SLOT(paneAdded(Pane *)));
+    connect(m_paneStack, SIGNAL(paneHidden(Pane *)),
+            this, SLOT(paneHidden(Pane *)));
+    connect(m_paneStack, SIGNAL(paneAboutToBeDeleted(Pane *)),
+            this, SLOT(paneAboutToBeDeleted(Pane *)));
+    connect(m_paneStack, SIGNAL(dropAccepted(Pane *, QStringList)),
+            this, SLOT(paneDropAccepted(Pane *, QStringList)));
+    connect(m_paneStack, SIGNAL(dropAccepted(Pane *, QString)),
+            this, SLOT(paneDropAccepted(Pane *, QString)));
+
+    m_playSource = new AudioCallbackPlaySource(m_viewManager);
+
+    connect(m_playSource, SIGNAL(sampleRateMismatch(size_t, size_t, bool)),
+	    this,           SLOT(sampleRateMismatch(size_t, size_t, bool)));
+    connect(m_playSource, SIGNAL(audioOverloadPluginDisabled()),
+            this,           SLOT(audioOverloadPluginDisabled()));
+
+    connect(m_viewManager, SIGNAL(outputLevelsChanged(float, float)),
+	    this, SLOT(outputLevelsChanged(float, float)));
+
+    connect(m_viewManager, SIGNAL(playbackFrameChanged(unsigned long)),
+            this, SLOT(playbackFrameChanged(unsigned long)));
+
+    connect(m_viewManager, SIGNAL(globalCentreFrameChanged(unsigned long)),
+            this, SLOT(globalCentreFrameChanged(unsigned long)));
+
+    connect(m_viewManager, SIGNAL(viewCentreFrameChanged(View *, unsigned long)),
+            this, SLOT(viewCentreFrameChanged(View *, unsigned long)));
+
+    connect(m_viewManager, SIGNAL(viewZoomLevelChanged(View *, unsigned long, bool)),
+            this, SLOT(viewZoomLevelChanged(View *, unsigned long, bool)));
+
+    connect(Preferences::getInstance(),
+            SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
+            this,
+            SLOT(preferenceChanged(PropertyContainer::PropertyName)));
+
+    if (m_oscQueue && m_oscQueue->isOK()) {
+        connect(m_oscQueue, SIGNAL(messagesAvailable()), this, SLOT(pollOSC()));
+        QTimer *oscTimer = new QTimer(this);
+        connect(oscTimer, SIGNAL(timeout()), this, SLOT(pollOSC()));
+        oscTimer->start(1000);
+    }
+
+    Labeller::ValueType labellerType = Labeller::ValueFromTwoLevelCounter;
+    QSettings settings;
+    settings.beginGroup("MainWindow");
+    labellerType = (Labeller::ValueType)
+        settings.value("labellertype", (int)labellerType).toInt();
+    int cycle = settings.value("labellercycle", 4).toInt();
+    settings.endGroup();
+
+    m_labeller = new Labeller(labellerType);
+    m_labeller->setCounterCycleSize(cycle);
+}
+
+MainWindowBase::~MainWindowBase()
+{
+    delete m_playTarget;
+    delete m_playSource;
+    delete m_viewManager;
+    delete m_oscQueue;
+    Profiles::getInstance()->dump();
+}
+
+QString
+MainWindowBase::getOpenFileName(FileFinder::FileType type)
+{
+    FileFinder *ff = FileFinder::getInstance();
+    switch (type) {
+    case FileFinder::SessionFile:
+        return ff->getOpenFileName(type, m_sessionFile);
+    case FileFinder::AudioFile:
+        return ff->getOpenFileName(type, m_audioFile);
+    case FileFinder::LayerFile:
+        return ff->getOpenFileName(type, m_sessionFile);
+    case FileFinder::LayerFileNoMidi:
+        return ff->getOpenFileName(type, m_sessionFile);
+    case FileFinder::SessionOrAudioFile:
+        return ff->getOpenFileName(type, m_sessionFile);
+    case FileFinder::ImageFile:
+        return ff->getOpenFileName(type, m_sessionFile);
+    case FileFinder::AnyFile:
+        if (getMainModel() != 0 &&
+            m_paneStack != 0 &&
+            m_paneStack->getCurrentPane() != 0) { // can import a layer
+            return ff->getOpenFileName(FileFinder::AnyFile, m_sessionFile);
+        } else {
+            return ff->getOpenFileName(FileFinder::SessionOrAudioFile,
+                                       m_sessionFile);
+        }
+    }
+    return "";
+}
+
+QString
+MainWindowBase::getSaveFileName(FileFinder::FileType type)
+{
+    FileFinder *ff = FileFinder::getInstance();
+    switch (type) {
+    case FileFinder::SessionFile:
+        return ff->getSaveFileName(type, m_sessionFile);
+    case FileFinder::AudioFile:
+        return ff->getSaveFileName(type, m_audioFile);
+    case FileFinder::LayerFile:
+        return ff->getSaveFileName(type, m_sessionFile);
+    case FileFinder::LayerFileNoMidi:
+        return ff->getSaveFileName(type, m_sessionFile);
+    case FileFinder::SessionOrAudioFile:
+        return ff->getSaveFileName(type, m_sessionFile);
+    case FileFinder::ImageFile:
+        return ff->getSaveFileName(type, m_sessionFile);
+    case FileFinder::AnyFile:
+        return ff->getSaveFileName(type, m_sessionFile);
+    }
+    return "";
+}
+
+void
+MainWindowBase::registerLastOpenedFilePath(FileFinder::FileType type, QString path)
+{
+    FileFinder *ff = FileFinder::getInstance();
+    ff->registerLastOpenedFilePath(type, path);
+}
+
+void
+MainWindowBase::updateMenuStates()
+{
+    Pane *currentPane = 0;
+    Layer *currentLayer = 0;
+
+    if (m_paneStack) currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentLayer = currentPane->getSelectedLayer();
+
+    bool haveCurrentPane =
+        (currentPane != 0);
+    bool haveCurrentLayer =
+        (haveCurrentPane &&
+         (currentLayer != 0));
+    bool haveMainModel =
+	(getMainModel() != 0);
+    bool havePlayTarget =
+	(m_playTarget != 0);
+    bool haveSelection = 
+	(m_viewManager &&
+	 !m_viewManager->getSelections().empty());
+    bool haveCurrentEditableLayer =
+	(haveCurrentLayer &&
+	 currentLayer->isLayerEditable());
+    bool haveCurrentTimeInstantsLayer = 
+	(haveCurrentLayer &&
+	 dynamic_cast<TimeInstantLayer *>(currentLayer));
+    bool haveCurrentColour3DPlot =
+        (haveCurrentLayer &&
+         dynamic_cast<Colour3DPlotLayer *>(currentLayer));
+    bool haveClipboardContents =
+        (m_viewManager &&
+         !m_viewManager->getClipboard().empty());
+
+    emit canAddPane(haveMainModel);
+    emit canDeleteCurrentPane(haveCurrentPane);
+    emit canZoom(haveMainModel && haveCurrentPane);
+    emit canScroll(haveMainModel && haveCurrentPane);
+    emit canAddLayer(haveMainModel && haveCurrentPane);
+    emit canImportMoreAudio(haveMainModel);
+    emit canImportLayer(haveMainModel && haveCurrentPane);
+    emit canExportAudio(haveMainModel);
+    emit canExportLayer(haveMainModel &&
+                        (haveCurrentEditableLayer || haveCurrentColour3DPlot));
+    emit canExportImage(haveMainModel && haveCurrentPane);
+    emit canDeleteCurrentLayer(haveCurrentLayer);
+    emit canRenameLayer(haveCurrentLayer);
+    emit canEditLayer(haveCurrentEditableLayer);
+    emit canMeasureLayer(haveCurrentLayer);
+    emit canSelect(haveMainModel && haveCurrentPane);
+    emit canPlay(havePlayTarget);
+    emit canFfwd(true);
+    emit canRewind(true);
+    emit canPaste(haveCurrentEditableLayer && haveClipboardContents);
+    emit canInsertInstant(haveCurrentPane);
+    emit canInsertInstantsAtBoundaries(haveCurrentPane && haveSelection);
+    emit canRenumberInstants(haveCurrentTimeInstantsLayer && haveSelection);
+    emit canPlaySelection(haveMainModel && havePlayTarget && haveSelection);
+    emit canClearSelection(haveSelection);
+    emit canEditSelection(haveSelection && haveCurrentEditableLayer);
+    emit canSave(m_sessionFile != "" && m_documentModified);
+}
+
+void
+MainWindowBase::documentModified()
+{
+//    std::cerr << "MainWindowBase::documentModified" << std::endl;
+
+    if (!m_documentModified) {
+        //!!! this in subclass implementation?
+	setWindowTitle(tr("%1 (modified)").arg(windowTitle()));
+    }
+
+    m_documentModified = true;
+    updateMenuStates();
+}
+
+void
+MainWindowBase::documentRestored()
+{
+//    std::cerr << "MainWindowBase::documentRestored" << std::endl;
+
+    if (m_documentModified) {
+        //!!! this in subclass implementation?
+	QString wt(windowTitle());
+	wt.replace(tr(" (modified)"), "");
+	setWindowTitle(wt);
+    }
+
+    m_documentModified = false;
+    updateMenuStates();
+}
+
+void
+MainWindowBase::playLoopToggled()
+{
+    QAction *action = dynamic_cast<QAction *>(sender());
+    
+    if (action) {
+	m_viewManager->setPlayLoopMode(action->isChecked());
+    } else {
+	m_viewManager->setPlayLoopMode(!m_viewManager->getPlayLoopMode());
+    }
+}
+
+void
+MainWindowBase::playSelectionToggled()
+{
+    QAction *action = dynamic_cast<QAction *>(sender());
+    
+    if (action) {
+	m_viewManager->setPlaySelectionMode(action->isChecked());
+    } else {
+	m_viewManager->setPlaySelectionMode(!m_viewManager->getPlaySelectionMode());
+    }
+}
+
+void
+MainWindowBase::playSoloToggled()
+{
+    QAction *action = dynamic_cast<QAction *>(sender());
+    
+    if (action) {
+	m_viewManager->setPlaySoloMode(action->isChecked());
+    } else {
+	m_viewManager->setPlaySoloMode(!m_viewManager->getPlaySoloMode());
+    }
+
+    if (m_viewManager->getPlaySoloMode()) {
+        currentPaneChanged(m_paneStack->getCurrentPane());
+    } else {
+        m_viewManager->setPlaybackModel(0);
+        if (m_playSource) {
+            m_playSource->clearSoloModelSet();
+        }
+    }
+}
+
+void
+MainWindowBase::currentPaneChanged(Pane *p)
+{
+    updateMenuStates();
+    updateVisibleRangeDisplay(p);
+
+    if (!p) return;
+
+    if (!(m_viewManager &&
+          m_playSource &&
+          m_viewManager->getPlaySoloMode())) {
+        if (m_viewManager) m_viewManager->setPlaybackModel(0);
+        return;
+    }
+
+    Model *prevPlaybackModel = m_viewManager->getPlaybackModel();
+
+    View::ModelSet soloModels = p->getModels();
+    
+    for (View::ModelSet::iterator mi = soloModels.begin();
+         mi != soloModels.end(); ++mi) {
+        if (dynamic_cast<RangeSummarisableTimeValueModel *>(*mi)) {
+            m_viewManager->setPlaybackModel(*mi);
+        }
+    }
+    
+    RangeSummarisableTimeValueModel *a = 
+        dynamic_cast<RangeSummarisableTimeValueModel *>(prevPlaybackModel);
+    RangeSummarisableTimeValueModel *b = 
+        dynamic_cast<RangeSummarisableTimeValueModel *>(m_viewManager->
+                                                        getPlaybackModel());
+
+    m_playSource->setSoloModelSet(soloModels);
+
+    if (a && b && (a != b)) {
+        int frame = m_playSource->getCurrentPlayingFrame();
+        //!!! I don't really believe that these functions are the right way around
+        int rframe = a->alignFromReference(frame);
+        int bframe = b->alignToReference(rframe);
+        if (m_playSource->isPlaying()) m_playSource->play(bframe);
+    }
+}
+
+void
+MainWindowBase::currentLayerChanged(Pane *p, Layer *)
+{
+    updateMenuStates();
+    updateVisibleRangeDisplay(p);
+}
+
+void
+MainWindowBase::selectAll()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setSelection(Selection(getMainModel()->getStartFrame(),
+					  getMainModel()->getEndFrame()));
+}
+
+void
+MainWindowBase::selectToStart()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setSelection(Selection(getMainModel()->getStartFrame(),
+					  m_viewManager->getGlobalCentreFrame()));
+}
+
+void
+MainWindowBase::selectToEnd()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setSelection(Selection(m_viewManager->getGlobalCentreFrame(),
+					  getMainModel()->getEndFrame()));
+}
+
+void
+MainWindowBase::selectVisible()
+{
+    Model *model = getMainModel();
+    if (!model) return;
+
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    size_t startFrame, endFrame;
+
+    if (currentPane->getStartFrame() < 0) startFrame = 0;
+    else startFrame = currentPane->getStartFrame();
+
+    if (currentPane->getEndFrame() > model->getEndFrame()) endFrame = model->getEndFrame();
+    else endFrame = currentPane->getEndFrame();
+
+    m_viewManager->setSelection(Selection(startFrame, endFrame));
+}
+
+void
+MainWindowBase::clearSelection()
+{
+    m_viewManager->clearSelections();
+}
+
+void
+MainWindowBase::cut()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    Layer *layer = currentPane->getSelectedLayer();
+    if (!layer) return;
+
+    Clipboard &clipboard = m_viewManager->getClipboard();
+    clipboard.clear();
+
+    MultiSelection::SelectionList selections = m_viewManager->getSelections();
+
+    CommandHistory::getInstance()->startCompoundOperation(tr("Cut"), true);
+
+    for (MultiSelection::SelectionList::iterator i = selections.begin();
+         i != selections.end(); ++i) {
+        layer->copy(*i, clipboard);
+        layer->deleteSelection(*i);
+    }
+
+    CommandHistory::getInstance()->endCompoundOperation();
+}
+
+void
+MainWindowBase::copy()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    Layer *layer = currentPane->getSelectedLayer();
+    if (!layer) return;
+
+    Clipboard &clipboard = m_viewManager->getClipboard();
+    clipboard.clear();
+
+    MultiSelection::SelectionList selections = m_viewManager->getSelections();
+
+    for (MultiSelection::SelectionList::iterator i = selections.begin();
+         i != selections.end(); ++i) {
+        layer->copy(*i, clipboard);
+    }
+}
+
+void
+MainWindowBase::paste()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    //!!! if we have no current layer, we should create one of the most
+    // appropriate type
+
+    Layer *layer = currentPane->getSelectedLayer();
+    if (!layer) return;
+
+    Clipboard &clipboard = m_viewManager->getClipboard();
+    Clipboard::PointList contents = clipboard.getPoints();
+/*
+    long minFrame = 0;
+    bool have = false;
+    for (int i = 0; i < contents.size(); ++i) {
+        if (!contents[i].haveFrame()) continue;
+        if (!have || contents[i].getFrame() < minFrame) {
+            minFrame = contents[i].getFrame();
+            have = true;
+        }
+    }
+
+    long frameOffset = long(m_viewManager->getGlobalCentreFrame()) - minFrame;
+
+    layer->paste(clipboard, frameOffset);
+*/
+    layer->paste(clipboard, 0, true);
+}
+
+void
+MainWindowBase::deleteSelected()
+{
+    if (m_paneStack->getCurrentPane() &&
+	m_paneStack->getCurrentPane()->getSelectedLayer()) {
+        
+        Layer *layer = m_paneStack->getCurrentPane()->getSelectedLayer();
+
+        if (m_viewManager && 
+            (m_viewManager->getToolMode() == ViewManager::MeasureMode)) {
+
+            layer->deleteCurrentMeasureRect();
+
+        } else {
+
+            MultiSelection::SelectionList selections =
+                m_viewManager->getSelections();
+            
+            for (MultiSelection::SelectionList::iterator i = selections.begin();
+                 i != selections.end(); ++i) {
+                layer->deleteSelection(*i);
+            }
+	}
+    }
+}
+
+void
+MainWindowBase::insertInstant()
+{
+    int frame = m_viewManager->getPlaybackFrame();
+    insertInstantAt(frame);
+}
+
+void
+MainWindowBase::insertInstantsAtBoundaries()
+{
+    MultiSelection::SelectionList selections = m_viewManager->getSelections();
+    for (MultiSelection::SelectionList::iterator i = selections.begin();
+         i != selections.end(); ++i) {
+        size_t start = i->getStartFrame();
+        size_t end = i->getEndFrame();
+        if (start != end) {
+            insertInstantAt(i->getStartFrame());
+            insertInstantAt(i->getEndFrame());
+        }
+    }
+}
+
+void
+MainWindowBase::insertInstantAt(size_t frame)
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (!pane) {
+        return;
+    }
+
+    Layer *layer = dynamic_cast<TimeInstantLayer *>
+        (pane->getSelectedLayer());
+
+    if (!layer) {
+        for (int i = pane->getLayerCount(); i > 0; --i) {
+            layer = dynamic_cast<TimeInstantLayer *>(pane->getLayer(i - 1));
+            if (layer) break;
+        }
+
+        if (!layer) {
+            CommandHistory::getInstance()->startCompoundOperation
+                (tr("Add Point"), true);
+            layer = m_document->createEmptyLayer(LayerFactory::TimeInstants);
+            if (layer) {
+                m_document->addLayerToView(pane, layer);
+                m_paneStack->setCurrentLayer(pane, layer);
+            }
+            CommandHistory::getInstance()->endCompoundOperation();
+        }
+    }
+
+    if (layer) {
+    
+        Model *model = layer->getModel();
+        SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
+            (model);
+
+        if (sodm) {
+            SparseOneDimensionalModel::Point point(frame, "");
+
+            SparseOneDimensionalModel::Point prevPoint(0);
+            bool havePrevPoint = false;
+
+            SparseOneDimensionalModel::EditCommand *command =
+                new SparseOneDimensionalModel::EditCommand(sodm, tr("Add Point"));
+
+            if (m_labeller->actingOnPrevPoint()) {
+
+                SparseOneDimensionalModel::PointList prevPoints =
+                    sodm->getPreviousPoints(frame);
+
+                if (!prevPoints.empty()) {
+                    prevPoint = *prevPoints.begin();
+                    havePrevPoint = true;
+                }
+            }
+
+            if (m_labeller) {
+
+                m_labeller->setSampleRate(sodm->getSampleRate());
+
+                if (havePrevPoint) {
+                    command->deletePoint(prevPoint);
+                }
+
+                m_labeller->label<SparseOneDimensionalModel::Point>
+                    (point, havePrevPoint ? &prevPoint : 0);
+
+                if (havePrevPoint) {
+                    command->addPoint(prevPoint);
+                }
+            }
+            
+            command->addPoint(point);
+
+            command->setName(tr("Add Point at %1 s")
+                             .arg(RealTime::frame2RealTime
+                                  (frame,
+                                   sodm->getSampleRate())
+                                  .toText(false).c_str()));
+
+            command->finish();
+        }
+    }
+}
+
+void
+MainWindowBase::renumberInstants()
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (!pane) return;
+
+    Layer *layer = dynamic_cast<TimeInstantLayer *>(pane->getSelectedLayer());
+    if (!layer) return;
+
+    MultiSelection ms(m_viewManager->getSelection());
+    
+    Model *model = layer->getModel();
+    SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
+        (model);
+    if (!sodm) return;
+
+    if (!m_labeller) return;
+
+    Labeller labeller(*m_labeller);
+    labeller.setSampleRate(sodm->getSampleRate());
+
+    // This uses a command
+
+    labeller.labelAll<SparseOneDimensionalModel::Point>(*sodm, &ms);
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::open(QString fileOrUrl, AudioFileOpenMode mode)
+{
+    return open(FileSource(fileOrUrl), mode);
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::open(FileSource source, AudioFileOpenMode mode)
+{
+    FileOpenStatus status;
+
+    if (!source.isAvailable()) return FileOpenFailed;
+    source.waitForData();
+
+    bool canImportLayer = (getMainModel() != 0 &&
+                           m_paneStack != 0 &&
+                           m_paneStack->getCurrentPane() != 0);
+
+    if ((status = openAudio(source, mode)) != FileOpenFailed) {
+        return status;
+    } else if ((status = openSession(source)) != FileOpenFailed) {
+	return status;
+    } else if ((status = openPlaylist(source, mode)) != FileOpenFailed) {
+        return status;
+    } else if (!canImportLayer) {
+        return FileOpenWrongMode;
+    } else if ((status = openImage(source)) != FileOpenFailed) {
+        return status;
+    } else if ((status = openLayer(source)) != FileOpenFailed) {
+        return status;
+    } else {
+	return FileOpenFailed;
+    }
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::openAudio(FileSource source, AudioFileOpenMode mode)
+{
+    std::cerr << "MainWindowBase::openAudio(" << source.getLocation().toStdString() << ")" << std::endl;
+
+    if (!source.isAvailable()) return FileOpenFailed;
+    source.waitForData();
+
+    m_openingAudioFile = true;
+
+    size_t rate = 0;
+
+    if (Preferences::getInstance()->getResampleOnLoad()) {
+        rate = m_playSource->getSourceSampleRate();
+    }
+
+    WaveFileModel *newModel = new WaveFileModel(source, rate);
+
+    if (!newModel->isOK()) {
+	delete newModel;
+        m_openingAudioFile = false;
+	return FileOpenFailed;
+    }
+
+    std::cerr << "mode = " << mode << std::endl;
+
+    if (mode == AskUser) {
+        if (getMainModel()) {
+
+            static bool prevSetAsMain = true;
+            bool setAsMain = true;
+            
+            QStringList items;
+            items << tr("Replace the existing main waveform")
+                  << tr("Load this file into a new waveform pane");
+
+            bool ok = false;
+            QString item = ListInputDialog::getItem
+                (this, tr("Select target for import"),
+                 tr("You already have an audio waveform loaded.\nWhat would you like to do with the new audio file?"),
+                 items, prevSetAsMain ? 0 : 1, &ok);
+            
+            if (!ok || item.isEmpty()) {
+                delete newModel;
+                m_openingAudioFile = false;
+                return FileOpenCancelled;
+            }
+            
+            setAsMain = (item == items[0]);
+            prevSetAsMain = setAsMain;
+
+            if (setAsMain) mode = ReplaceMainModel;
+            else mode = CreateAdditionalModel;
+
+        } else {
+            mode = ReplaceMainModel;
+        }
+    }
+
+    if (mode == ReplaceCurrentPane) {
+
+        Pane *pane = m_paneStack->getCurrentPane();
+        if (pane) {
+            if (getMainModel()) {
+                View::ModelSet models(pane->getModels());
+                if (models.find(getMainModel()) != models.end()) {
+                    mode = ReplaceMainModel;
+                }
+            } else {
+                mode = ReplaceMainModel;
+            }
+        } else {
+            mode = CreateAdditionalModel;
+        }
+    }
+
+    if (mode == CreateAdditionalModel && !getMainModel()) {
+        mode = ReplaceMainModel;
+    }
+
+    if (mode == ReplaceMainModel) {
+
+        Model *prevMain = getMainModel();
+        if (prevMain) {
+            m_playSource->removeModel(prevMain);
+            PlayParameterRepository::getInstance()->removeModel(prevMain);
+        }
+        PlayParameterRepository::getInstance()->addModel(newModel);
+
+	m_document->setMainModel(newModel);
+
+	setupMenus();
+
+	if (m_sessionFile == "") {
+            //!!! shouldn't be dealing directly with title from here -- call a method
+	    setWindowTitle(tr("Sonic Visualiser: %1")
+                           .arg(source.getLocation()));
+	    CommandHistory::getInstance()->clear();
+	    CommandHistory::getInstance()->documentSaved();
+	    m_documentModified = false;
+	} else {
+	    setWindowTitle(tr("Sonic Visualiser: %1 [%2]")
+			   .arg(QFileInfo(m_sessionFile).fileName())
+			   .arg(source.getLocation()));
+	    if (m_documentModified) {
+		m_documentModified = false;
+		documentModified(); // so as to restore "(modified)" window title
+	    }
+	}
+
+        if (!source.isRemote()) m_audioFile = source.getLocalFilename();
+
+    } else if (mode == CreateAdditionalModel) {
+
+	CommandHistory::getInstance()->startCompoundOperation
+	    (tr("Import \"%1\"").arg(source.getLocation()), true);
+
+	m_document->addImportedModel(newModel);
+
+	AddPaneCommand *command = new AddPaneCommand(this);
+	CommandHistory::getInstance()->addCommand(command);
+
+	Pane *pane = command->getPane();
+
+	if (!m_timeRulerLayer) {
+	    m_timeRulerLayer = m_document->createMainModelLayer
+		(LayerFactory::TimeRuler);
+	}
+
+	m_document->addLayerToView(pane, m_timeRulerLayer);
+
+	Layer *newLayer = m_document->createImportedLayer(newModel);
+
+	if (newLayer) {
+	    m_document->addLayerToView(pane, newLayer);
+	}
+	
+	CommandHistory::getInstance()->endCompoundOperation();
+
+    } else if (mode == ReplaceCurrentPane) {
+
+        // We know there is a current pane, otherwise we would have
+        // reset the mode to CreateAdditionalModel above; and we know
+        // the current pane does not contain the main model, otherwise
+        // we would have reset it to ReplaceMainModel.  But we don't
+        // know whether the pane contains a waveform model at all.
+        
+        Pane *pane = m_paneStack->getCurrentPane();
+        Layer *replace = 0;
+
+        for (int i = 0; i < pane->getLayerCount(); ++i) {
+            Layer *layer = pane->getLayer(i);
+            if (dynamic_cast<WaveformLayer *>(layer)) {
+                replace = layer;
+                break;
+            }
+        }
+
+	CommandHistory::getInstance()->startCompoundOperation
+	    (tr("Import \"%1\"").arg(source.getLocation()), true);
+
+	m_document->addImportedModel(newModel);
+
+        if (replace) {
+            m_document->removeLayerFromView(pane, replace);
+        }
+
+	Layer *newLayer = m_document->createImportedLayer(newModel);
+
+	if (newLayer) {
+	    m_document->addLayerToView(pane, newLayer);
+	}
+	
+	CommandHistory::getInstance()->endCompoundOperation();
+    }
+
+    updateMenuStates();
+    m_recentFiles.addFile(source.getLocation());
+    if (!source.isRemote()) {
+        // for file dialog
+        registerLastOpenedFilePath(FileFinder::AudioFile,
+                                   source.getLocalFilename());
+    }
+    m_openingAudioFile = false;
+
+    currentPaneChanged(m_paneStack->getCurrentPane());
+
+    return FileOpenSucceeded;
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::openPlaylist(FileSource source, AudioFileOpenMode mode)
+{
+    std::set<QString> extensions;
+    PlaylistFileReader::getSupportedExtensions(extensions);
+    QString extension = source.getExtension();
+    if (extensions.find(extension) == extensions.end()) return FileOpenFailed;
+
+    if (!source.isAvailable()) return FileOpenFailed;
+    source.waitForData();
+
+    PlaylistFileReader reader(source.getLocalFilename());
+    if (!reader.isOK()) return FileOpenFailed;
+
+    PlaylistFileReader::Playlist playlist = reader.load();
+
+    bool someSuccess = false;
+
+    for (PlaylistFileReader::Playlist::const_iterator i = playlist.begin();
+         i != playlist.end(); ++i) {
+
+        FileOpenStatus status = openAudio(*i, mode);
+
+        if (status == FileOpenCancelled) {
+            return FileOpenCancelled;
+        }
+
+        if (status == FileOpenSucceeded) {
+            someSuccess = true;
+            mode = CreateAdditionalModel;
+        }
+    }
+
+    if (someSuccess) return FileOpenSucceeded;
+    else return FileOpenFailed;
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::openLayer(FileSource source)
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    
+    if (!pane) {
+	// shouldn't happen, as the menu action should have been disabled
+	std::cerr << "WARNING: MainWindowBase::openLayer: no current pane" << std::endl;
+	return FileOpenWrongMode;
+    }
+
+    if (!getMainModel()) {
+	// shouldn't happen, as the menu action should have been disabled
+	std::cerr << "WARNING: MainWindowBase::openLayer: No main model -- hence no default sample rate available" << std::endl;
+	return FileOpenWrongMode;
+    }
+
+    if (!source.isAvailable()) return FileOpenFailed;
+    source.waitForData();
+
+    QString path = source.getLocalFilename();
+
+    if (source.getExtension() == "svl" || source.getExtension() == "xml") {
+
+        PaneCallback callback(this);
+        QFile file(path);
+        
+        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+            std::cerr << "ERROR: MainWindowBase::openLayer("
+                      << source.getLocation().toStdString()
+                      << "): Failed to open file for reading" << std::endl;
+            return FileOpenFailed;
+        }
+        
+        SVFileReader reader(m_document, callback, source.getLocation());
+        reader.setCurrentPane(pane);
+        
+        QXmlInputSource inputSource(&file);
+        reader.parse(inputSource);
+        
+        if (!reader.isOK()) {
+            std::cerr << "ERROR: MainWindowBase::openLayer("
+                      << source.getLocation().toStdString()
+                      << "): Failed to read XML file: "
+                      << reader.getErrorString().toStdString() << std::endl;
+            return FileOpenFailed;
+        }
+
+        m_recentFiles.addFile(source.getLocation());
+
+        if (!source.isRemote()) {
+            registerLastOpenedFilePath(FileFinder::LayerFile, path); // for file dialog
+        }
+
+    } else {
+        
+        try {
+
+            Model *model = DataFileReaderFactory::load
+                (path, getMainModel()->getSampleRate());
+        
+            if (model) {
+
+                std::cerr << "MainWindowBase::openLayer: Have model" << std::endl;
+
+                Layer *newLayer = m_document->createImportedLayer(model);
+
+                if (newLayer) {
+
+                    m_document->addLayerToView(pane, newLayer);
+                    m_recentFiles.addFile(source.getLocation());
+                    
+                    if (!source.isRemote()) {
+                        registerLastOpenedFilePath
+                            (FileFinder::LayerFile,
+                             path); // for file dialog
+                    }
+                    
+                    return FileOpenSucceeded;
+                }
+            }
+        } catch (DataFileReaderFactory::Exception e) {
+            if (e == DataFileReaderFactory::ImportCancelled) {
+                return FileOpenCancelled;
+            }
+        }
+    }
+    
+    source.setLeaveLocalFile(true);
+    return FileOpenFailed;
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::openImage(FileSource source)
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    
+    if (!pane) {
+	// shouldn't happen, as the menu action should have been disabled
+	std::cerr << "WARNING: MainWindowBase::openImage: no current pane" << std::endl;
+	return FileOpenWrongMode;
+    }
+
+    if (!m_document->getMainModel()) {
+        return FileOpenWrongMode;
+    }
+
+    bool newLayer = false;
+
+    ImageLayer *il = dynamic_cast<ImageLayer *>(pane->getSelectedLayer());
+    if (!il) {
+        for (int i = pane->getLayerCount()-1; i >= 0; --i) {
+            il = dynamic_cast<ImageLayer *>(pane->getLayer(i));
+            if (il) break;
+        }
+    }
+    if (!il) {
+        il = dynamic_cast<ImageLayer *>
+            (m_document->createEmptyLayer(LayerFactory::Image));
+        if (!il) return FileOpenFailed;
+        newLayer = true;
+    }
+
+    // We don't put the image file in Recent Files
+
+    std::cerr << "openImage: trying location \"" << source.getLocation().toStdString() << "\" in image layer" << std::endl;
+
+    if (!il->addImage(m_viewManager->getGlobalCentreFrame(), source.getLocation())) {
+        if (newLayer) {
+            m_document->setModel(il, 0); // releasing its model
+            delete il;
+        }
+        return FileOpenFailed;
+    } else {
+        if (newLayer) {
+            m_document->addLayerToView(pane, il);
+        }
+        m_paneStack->setCurrentLayer(pane, il);
+    }
+
+    return FileOpenSucceeded;
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::openSessionFile(QString fileOrUrl)
+{
+    return openSession(FileSource(fileOrUrl));
+}
+
+MainWindowBase::FileOpenStatus
+MainWindowBase::openSession(FileSource source)
+{
+    if (!source.isAvailable()) return FileOpenFailed;
+    if (source.getExtension() != "sv") return FileOpenFailed;
+    source.waitForData();
+
+    BZipFileDevice bzFile(source.getLocalFilename());
+    if (!bzFile.open(QIODevice::ReadOnly)) return FileOpenFailed;
+
+    if (!checkSaveModified()) return FileOpenCancelled;
+
+    QString error;
+    closeSession();
+    createDocument();
+
+    PaneCallback callback(this);
+    m_viewManager->clearSelections();
+
+    SVFileReader reader(m_document, callback, source.getLocation());
+    QXmlInputSource inputSource(&bzFile);
+    reader.parse(inputSource);
+    
+    if (!reader.isOK()) {
+        error = tr("SV XML file read error:\n%1").arg(reader.getErrorString());
+    }
+    
+    bzFile.close();
+
+    bool ok = (error == "");
+
+    if (ok) {
+
+	setWindowTitle(tr("Sonic Visualiser: %1")
+		       .arg(source.getLocation()));
+
+	if (!source.isRemote()) m_sessionFile = source.getLocalFilename();
+
+	setupMenus();
+
+	CommandHistory::getInstance()->clear();
+	CommandHistory::getInstance()->documentSaved();
+	m_documentModified = false;
+	updateMenuStates();
+
+        m_recentFiles.addFile(source.getLocation());
+
+        if (!source.isRemote()) {
+            // for file dialog
+            registerLastOpenedFilePath(FileFinder::SessionFile,
+                                        source.getLocalFilename());
+        }
+
+    } else {
+	setWindowTitle(tr("Sonic Visualiser"));
+    }
+
+    return ok ? FileOpenSucceeded : FileOpenFailed;
+}
+
+void
+MainWindowBase::createPlayTarget()
+{
+    if (m_playTarget) return;
+
+    m_playTarget = AudioTargetFactory::createCallbackTarget(m_playSource);
+    if (!m_playTarget) {
+	QMessageBox::warning
+	    (this, tr("Couldn't open audio device"),
+	     tr("<b>No audio available</b><p>Could not open an audio device for playback.<p>Audio playback will not be available during this session."),
+	     QMessageBox::Ok);
+    }
+}
+
+WaveFileModel *
+MainWindowBase::getMainModel()
+{
+    if (!m_document) return 0;
+    return m_document->getMainModel();
+}
+
+const WaveFileModel *
+MainWindowBase::getMainModel() const
+{
+    if (!m_document) return 0;
+    return m_document->getMainModel();
+}
+
+void
+MainWindowBase::createDocument()
+{
+    m_document = new Document;
+
+    connect(m_document, SIGNAL(layerAdded(Layer *)),
+	    this, SLOT(layerAdded(Layer *)));
+    connect(m_document, SIGNAL(layerRemoved(Layer *)),
+	    this, SLOT(layerRemoved(Layer *)));
+    connect(m_document, SIGNAL(layerAboutToBeDeleted(Layer *)),
+	    this, SLOT(layerAboutToBeDeleted(Layer *)));
+    connect(m_document, SIGNAL(layerInAView(Layer *, bool)),
+	    this, SLOT(layerInAView(Layer *, bool)));
+
+    connect(m_document, SIGNAL(modelAdded(Model *)),
+	    this, SLOT(modelAdded(Model *)));
+    connect(m_document, SIGNAL(mainModelChanged(WaveFileModel *)),
+	    this, SLOT(mainModelChanged(WaveFileModel *)));
+    connect(m_document, SIGNAL(modelAboutToBeDeleted(Model *)),
+	    this, SLOT(modelAboutToBeDeleted(Model *)));
+
+    connect(m_document, SIGNAL(modelGenerationFailed(QString)),
+            this, SLOT(modelGenerationFailed(QString)));
+    connect(m_document, SIGNAL(modelRegenerationFailed(QString, QString)),
+            this, SLOT(modelRegenerationFailed(QString, QString)));
+}
+
+bool
+MainWindowBase::saveSessionFile(QString path)
+{
+    BZipFileDevice bzFile(path);
+    if (!bzFile.open(QIODevice::WriteOnly)) {
+        std::cerr << "Failed to open session file \"" << path.toStdString()
+                  << "\" for writing: "
+                  << bzFile.errorString().toStdString() << std::endl;
+        return false;
+    }
+
+    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+
+    QTextStream out(&bzFile);
+    toXml(out);
+    out.flush();
+
+    QApplication::restoreOverrideCursor();
+
+    if (!bzFile.isOK()) {
+	QMessageBox::critical(this, tr("Failed to write file"),
+			      tr("<b>Save failed</b><p>Failed to write to file \"%1\": %2")
+			      .arg(path).arg(bzFile.errorString()));
+        bzFile.close();
+	return false;
+    }
+
+    bzFile.close();
+    return true;
+}
+
+void
+MainWindowBase::toXml(QTextStream &out)
+{
+    QString indent("  ");
+
+    out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
+    out << "<!DOCTYPE sonic-visualiser>\n";
+    out << "<sv>\n";
+
+    m_document->toXml(out, "", "");
+
+    out << "<display>\n";
+
+    out << QString("  <window width=\"%1\" height=\"%2\"/>\n")
+	.arg(width()).arg(height());
+
+    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
+
+	Pane *pane = m_paneStack->getPane(i);
+
+	if (pane) {
+            pane->toXml(out, indent);
+	}
+    }
+
+    out << "</display>\n";
+
+    m_viewManager->getSelection().toXml(out);
+
+    out << "</sv>\n";
+}
+
+Pane *
+MainWindowBase::addPaneToStack()
+{
+    AddPaneCommand *command = new AddPaneCommand(this);
+    CommandHistory::getInstance()->addCommand(command);
+    return command->getPane();
+}
+
+void
+MainWindowBase::zoomIn()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->zoom(true);
+}
+
+void
+MainWindowBase::zoomOut()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->zoom(false);
+}
+
+void
+MainWindowBase::zoomToFit()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    Model *model = getMainModel();
+    if (!model) return;
+    
+    size_t start = model->getStartFrame();
+    size_t end = model->getEndFrame();
+    size_t pixels = currentPane->width();
+
+    size_t sw = currentPane->getVerticalScaleWidth();
+    if (pixels > sw * 2) pixels -= sw * 2;
+    else pixels = 1;
+    if (pixels > 4) pixels -= 4;
+
+    size_t zoomLevel = (end - start) / pixels;
+
+    currentPane->setZoomLevel(zoomLevel);
+    currentPane->setCentreFrame((start + end) / 2);
+}
+
+void
+MainWindowBase::zoomDefault()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->setZoomLevel(1024);
+}
+
+void
+MainWindowBase::scrollLeft()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(false, false);
+}
+
+void
+MainWindowBase::jumpLeft()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(false, true);
+}
+
+void
+MainWindowBase::scrollRight()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(true, false);
+}
+
+void
+MainWindowBase::jumpRight()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(true, true);
+}
+
+void
+MainWindowBase::showNoOverlays()
+{
+    m_viewManager->setOverlayMode(ViewManager::NoOverlays);
+}
+
+void
+MainWindowBase::showMinimalOverlays()
+{
+    m_viewManager->setOverlayMode(ViewManager::MinimalOverlays);
+}
+
+void
+MainWindowBase::showStandardOverlays()
+{
+    m_viewManager->setOverlayMode(ViewManager::StandardOverlays);
+}
+
+void
+MainWindowBase::showAllOverlays()
+{
+    m_viewManager->setOverlayMode(ViewManager::AllOverlays);
+}
+
+void
+MainWindowBase::toggleZoomWheels()
+{
+    if (m_viewManager->getZoomWheelsEnabled()) {
+        m_viewManager->setZoomWheelsEnabled(false);
+    } else {
+        m_viewManager->setZoomWheelsEnabled(true);
+    }
+}
+
+void
+MainWindowBase::togglePropertyBoxes()
+{
+    if (m_paneStack->getLayoutStyle() == PaneStack::NoPropertyStacks) {
+        if (Preferences::getInstance()->getPropertyBoxLayout() ==
+            Preferences::VerticallyStacked) {
+            m_paneStack->setLayoutStyle(PaneStack::PropertyStackPerPaneLayout);
+        } else {
+            m_paneStack->setLayoutStyle(PaneStack::SinglePropertyStackLayout);
+        }
+    } else {
+        m_paneStack->setLayoutStyle(PaneStack::NoPropertyStacks);
+    }
+}
+
+void
+MainWindowBase::toggleStatusBar()
+{
+    QSettings settings;
+    settings.beginGroup("MainWindow");
+    bool sb = settings.value("showstatusbar", true).toBool();
+
+    if (sb) {
+        statusBar()->hide();
+    } else {
+        statusBar()->show();
+    }
+
+    settings.setValue("showstatusbar", !sb);
+
+    settings.endGroup();
+}
+
+void
+MainWindowBase::preferenceChanged(PropertyContainer::PropertyName name)
+{
+    if (name == "Property Box Layout") {
+        if (m_paneStack->getLayoutStyle() != PaneStack::NoPropertyStacks) {
+            if (Preferences::getInstance()->getPropertyBoxLayout() ==
+                Preferences::VerticallyStacked) {
+                m_paneStack->setLayoutStyle(PaneStack::PropertyStackPerPaneLayout);
+            } else {
+                m_paneStack->setLayoutStyle(PaneStack::SinglePropertyStackLayout);
+            }
+        }
+    } else if (name == "Background Mode" && m_viewManager) {
+        Preferences::BackgroundMode mode =
+            Preferences::getInstance()->getBackgroundMode();
+        if (mode == Preferences::BackgroundFromTheme) {
+            m_viewManager->setGlobalDarkBackground(m_initialDarkBackground);
+        } else if (mode == Preferences::DarkBackground) {
+            m_viewManager->setGlobalDarkBackground(true);
+        } else {
+            m_viewManager->setGlobalDarkBackground(false);
+        }
+    }            
+}
+
+void
+MainWindowBase::play()
+{
+    if (m_playSource->isPlaying()) {
+        stop();
+    } else {
+        playbackFrameChanged(m_viewManager->getPlaybackFrame());
+	m_playSource->play(m_viewManager->getPlaybackFrame());
+    }
+}
+
+void
+MainWindowBase::ffwd()
+{
+    if (!getMainModel()) return;
+
+    int frame = m_viewManager->getPlaybackFrame();
+    ++frame;
+
+    Layer *layer = getSnapLayer();
+    size_t sr = getMainModel()->getSampleRate();
+
+    if (!layer) {
+
+        frame = RealTime::realTime2Frame
+            (RealTime::frame2RealTime(frame, sr) + RealTime(2, 0), sr);
+        if (frame > int(getMainModel()->getEndFrame())) {
+            frame = getMainModel()->getEndFrame();
+        }
+
+    } else {
+
+        size_t resolution = 0;
+        if (!layer->snapToFeatureFrame(m_paneStack->getCurrentPane(),
+                                       frame, resolution, Layer::SnapRight)) {
+            frame = getMainModel()->getEndFrame();
+        }
+    }
+        
+    if (frame < 0) frame = 0;
+
+    if (m_viewManager->getPlaySelectionMode()) {
+        frame = m_viewManager->constrainFrameToSelection(size_t(frame));
+    }
+    
+    m_viewManager->setPlaybackFrame(frame);
+}
+
+void
+MainWindowBase::ffwdEnd()
+{
+    if (!getMainModel()) return;
+
+    size_t frame = getMainModel()->getEndFrame();
+
+    if (m_viewManager->getPlaySelectionMode()) {
+        frame = m_viewManager->constrainFrameToSelection(frame);
+    }
+
+    m_viewManager->setPlaybackFrame(frame);
+}
+
+void
+MainWindowBase::rewind()
+{
+    if (!getMainModel()) return;
+
+    int frame = m_viewManager->getPlaybackFrame();
+    if (frame > 0) --frame;
+
+    Layer *layer = getSnapLayer();
+    size_t sr = getMainModel()->getSampleRate();
+    
+    // when rewinding during playback, we want to allow a period
+    // following a rewind target point at which the rewind will go to
+    // the prior point instead of the immediately neighbouring one
+    if (m_playSource && m_playSource->isPlaying()) {
+        RealTime ct = RealTime::frame2RealTime(frame, sr);
+        ct = ct - RealTime::fromSeconds(0.25);
+        if (ct < RealTime::zeroTime) ct = RealTime::zeroTime;
+//        std::cerr << "rewind: frame " << frame << " -> ";
+        frame = RealTime::realTime2Frame(ct, sr);
+//        std::cerr << frame << std::endl;
+    }
+
+    if (!layer) {
+        
+        frame = RealTime::realTime2Frame
+            (RealTime::frame2RealTime(frame, sr) - RealTime(2, 0), sr);
+        if (frame < int(getMainModel()->getStartFrame())) {
+            frame = getMainModel()->getStartFrame();
+        }
+
+    } else {
+
+        size_t resolution = 0;
+        if (!layer->snapToFeatureFrame(m_paneStack->getCurrentPane(),
+                                       frame, resolution, Layer::SnapLeft)) {
+            frame = getMainModel()->getStartFrame();
+        }
+    }
+
+    if (frame < 0) frame = 0;
+
+    if (m_viewManager->getPlaySelectionMode()) {
+        frame = m_viewManager->constrainFrameToSelection(size_t(frame));
+    }
+
+    m_viewManager->setPlaybackFrame(frame);
+}
+
+void
+MainWindowBase::rewindStart()
+{
+    if (!getMainModel()) return;
+
+    size_t frame = getMainModel()->getStartFrame();
+
+    if (m_viewManager->getPlaySelectionMode()) {
+        frame = m_viewManager->constrainFrameToSelection(frame);
+    }
+
+    m_viewManager->setPlaybackFrame(frame);
+}
+
+Layer *
+MainWindowBase::getSnapLayer() const
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (!pane) return 0;
+
+    Layer *layer = pane->getSelectedLayer();
+
+    if (!dynamic_cast<TimeInstantLayer *>(layer) &&
+        !dynamic_cast<TimeValueLayer *>(layer) &&
+        !dynamic_cast<TimeRulerLayer *>(layer)) {
+
+        layer = 0;
+
+        for (int i = pane->getLayerCount(); i > 0; --i) {
+            Layer *l = pane->getLayer(i-1);
+            if (dynamic_cast<TimeRulerLayer *>(l)) {
+                layer = l;
+                break;
+            }
+        }
+    }
+
+    return layer;
+}
+
+void
+MainWindowBase::stop()
+{
+    m_playSource->stop();
+
+    if (m_paneStack && m_paneStack->getCurrentPane()) {
+        updateVisibleRangeDisplay(m_paneStack->getCurrentPane());
+    } else {
+        m_myStatusMessage = "";
+        statusBar()->showMessage("");
+    }
+}
+
+MainWindowBase::AddPaneCommand::AddPaneCommand(MainWindowBase *mw) :
+    m_mw(mw),
+    m_pane(0),
+    m_prevCurrentPane(0),
+    m_added(false)
+{
+}
+
+MainWindowBase::AddPaneCommand::~AddPaneCommand()
+{
+    if (m_pane && !m_added) {
+	m_mw->m_paneStack->deletePane(m_pane);
+    }
+}
+
+QString
+MainWindowBase::AddPaneCommand::getName() const
+{
+    return tr("Add Pane");
+}
+
+void
+MainWindowBase::AddPaneCommand::execute()
+{
+    if (!m_pane) {
+	m_prevCurrentPane = m_mw->m_paneStack->getCurrentPane();
+	m_pane = m_mw->m_paneStack->addPane();
+
+        connect(m_pane, SIGNAL(contextHelpChanged(const QString &)),
+                m_mw, SLOT(contextHelpChanged(const QString &)));
+    } else {
+	m_mw->m_paneStack->showPane(m_pane);
+    }
+
+    m_mw->m_paneStack->setCurrentPane(m_pane);
+    m_added = true;
+}
+
+void
+MainWindowBase::AddPaneCommand::unexecute()
+{
+    m_mw->m_paneStack->hidePane(m_pane);
+    m_mw->m_paneStack->setCurrentPane(m_prevCurrentPane);
+    m_added = false;
+}
+
+MainWindowBase::RemovePaneCommand::RemovePaneCommand(MainWindowBase *mw, Pane *pane) :
+    m_mw(mw),
+    m_pane(pane),
+    m_added(true)
+{
+}
+
+MainWindowBase::RemovePaneCommand::~RemovePaneCommand()
+{
+    if (m_pane && !m_added) {
+	m_mw->m_paneStack->deletePane(m_pane);
+    }
+}
+
+QString
+MainWindowBase::RemovePaneCommand::getName() const
+{
+    return tr("Remove Pane");
+}
+
+void
+MainWindowBase::RemovePaneCommand::execute()
+{
+    m_prevCurrentPane = m_mw->m_paneStack->getCurrentPane();
+    m_mw->m_paneStack->hidePane(m_pane);
+    m_added = false;
+}
+
+void
+MainWindowBase::RemovePaneCommand::unexecute()
+{
+    m_mw->m_paneStack->showPane(m_pane);
+    m_mw->m_paneStack->setCurrentPane(m_prevCurrentPane);
+    m_added = true;
+}
+
+void
+MainWindowBase::deleteCurrentPane()
+{
+    CommandHistory::getInstance()->startCompoundOperation
+	(tr("Delete Pane"), true);
+
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (pane) {
+	while (pane->getLayerCount() > 0) {
+	    Layer *layer = pane->getLayer(0);
+	    if (layer) {
+		m_document->removeLayerFromView(pane, layer);
+	    } else {
+		break;
+	    }
+	}
+
+	RemovePaneCommand *command = new RemovePaneCommand(this, pane);
+	CommandHistory::getInstance()->addCommand(command);
+    }
+
+    CommandHistory::getInstance()->endCompoundOperation();
+
+    updateMenuStates();
+}
+
+void
+MainWindowBase::deleteCurrentLayer()
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (pane) {
+	Layer *layer = pane->getSelectedLayer();
+	if (layer) {
+	    m_document->removeLayerFromView(pane, layer);
+	}
+    }
+    updateMenuStates();
+}
+
+void
+MainWindowBase::playbackFrameChanged(unsigned long frame)
+{
+    if (!(m_playSource && m_playSource->isPlaying()) || !getMainModel()) return;
+
+    RealTime now = RealTime::frame2RealTime
+        (frame, getMainModel()->getSampleRate());
+
+    if (now.sec == m_lastPlayStatusSec) return;
+
+    RealTime then = RealTime::frame2RealTime
+        (m_playSource->getPlayEndFrame(), getMainModel()->getSampleRate());
+
+    QString nowStr;
+    QString thenStr;
+    QString remainingStr;
+
+    if (then.sec > 10) {
+        nowStr = now.toSecText().c_str();
+        thenStr = then.toSecText().c_str();
+        remainingStr = (then - now).toSecText().c_str();
+        m_lastPlayStatusSec = now.sec;
+    } else {
+        nowStr = now.toText(true).c_str();
+        thenStr = then.toText(true).c_str();
+        remainingStr = (then - now).toText(true).c_str();
+    }        
+
+    m_myStatusMessage = tr("Playing: %1 of %2 (%3 remaining)")
+        .arg(nowStr).arg(thenStr).arg(remainingStr);
+
+    statusBar()->showMessage(m_myStatusMessage);
+}
+
+void
+MainWindowBase::globalCentreFrameChanged(unsigned long )
+{
+    if ((m_playSource && m_playSource->isPlaying()) || !getMainModel()) return;
+    Pane *p = 0;
+    if (!m_paneStack || !(p = m_paneStack->getCurrentPane())) return;
+    if (!p->getFollowGlobalPan()) return;
+    updateVisibleRangeDisplay(p);
+}
+
+void
+MainWindowBase::viewCentreFrameChanged(View *v, unsigned long )
+{
+    if ((m_playSource && m_playSource->isPlaying()) || !getMainModel()) return;
+    Pane *p = 0;
+    if (!m_paneStack || !(p = m_paneStack->getCurrentPane())) return;
+    if (v == p) updateVisibleRangeDisplay(p);
+}
+
+void
+MainWindowBase::viewZoomLevelChanged(View *v, unsigned long , bool )
+{
+    if ((m_playSource && m_playSource->isPlaying()) || !getMainModel()) return;
+    Pane *p = 0;
+    if (!m_paneStack || !(p = m_paneStack->getCurrentPane())) return;
+    if (v == p) updateVisibleRangeDisplay(p);
+}
+
+void
+MainWindowBase::layerAdded(Layer *)
+{
+//    std::cerr << "MainWindowBase::layerAdded(" << layer << ")" << std::endl;
+    updateMenuStates();
+}
+
+void
+MainWindowBase::layerRemoved(Layer *)
+{
+//    std::cerr << "MainWindowBase::layerRemoved(" << layer << ")" << std::endl;
+    updateMenuStates();
+}
+
+void
+MainWindowBase::layerAboutToBeDeleted(Layer *layer)
+{
+//    std::cerr << "MainWindowBase::layerAboutToBeDeleted(" << layer << ")" << std::endl;
+    if (layer == m_timeRulerLayer) {
+//	std::cerr << "(this is the time ruler layer)" << std::endl;
+	m_timeRulerLayer = 0;
+    }
+}
+
+void
+MainWindowBase::layerInAView(Layer *layer, bool inAView)
+{
+//    std::cerr << "MainWindowBase::layerInAView(" << layer << "," << inAView << ")" << std::endl;
+
+    // Check whether we need to add or remove model from play source
+    Model *model = layer->getModel();
+    if (model) {
+        if (inAView) {
+            m_playSource->addModel(model);
+        } else {
+            bool found = false;
+            for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
+                Pane *pane = m_paneStack->getPane(i);
+                if (!pane) continue;
+                for (int j = 0; j < pane->getLayerCount(); ++j) {
+                    Layer *pl = pane->getLayer(j);
+                    if (pl && pl->getModel() == model) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (found) break;
+            }
+            if (!found) m_playSource->removeModel(model);
+        }
+    }
+
+    updateMenuStates();
+}
+
+void
+MainWindowBase::modelAdded(Model *model)
+{
+//    std::cerr << "MainWindowBase::modelAdded(" << model << ")" << std::endl;
+    m_playSource->addModel(model);
+}
+
+void
+MainWindowBase::mainModelChanged(WaveFileModel *model)
+{
+//    std::cerr << "MainWindowBase::mainModelChanged(" << model << ")" << std::endl;
+    updateDescriptionLabel();
+    if (model) m_viewManager->setMainModelSampleRate(model->getSampleRate());
+    if (model && !m_playTarget && m_audioOutput) createPlayTarget();
+}
+
+void
+MainWindowBase::modelAboutToBeDeleted(Model *model)
+{
+//    std::cerr << "MainWindowBase::modelAboutToBeDeleted(" << model << ")" << std::endl;
+    if (model == m_viewManager->getPlaybackModel()) {
+        m_viewManager->setPlaybackModel(0);
+    }
+    m_playSource->removeModel(model);
+    FFTDataServer::modelAboutToBeDeleted(model);
+}
+
+void
+MainWindowBase::pollOSC()
+{
+    if (!m_oscQueue || m_oscQueue->isEmpty()) return;
+    std::cerr << "MainWindowBase::pollOSC: have " << m_oscQueue->getMessagesAvailable() << " messages" << std::endl;
+
+    if (m_openingAudioFile) return;
+
+    OSCMessage message = m_oscQueue->readMessage();
+
+    if (message.getTarget() != 0) {
+        return; //!!! for now -- this class is target 0, others not handled yet
+    }
+
+    handleOSCMessage(message);
+}
+
+void
+MainWindowBase::inProgressSelectionChanged()
+{
+    Pane *currentPane = 0;
+    if (m_paneStack) currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) updateVisibleRangeDisplay(currentPane);
+}
+
+void
+MainWindowBase::contextHelpChanged(const QString &s)
+{
+    if (s == "" && m_myStatusMessage != "") {
+        statusBar()->showMessage(m_myStatusMessage);
+        return;
+    }
+    statusBar()->showMessage(s);
+}
+
+void
+MainWindowBase::openHelpUrl(QString url)
+{
+    // This method mostly lifted from Qt Assistant source code
+
+    QProcess *process = new QProcess(this);
+    connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
+
+    QStringList args;
+
+#ifdef Q_OS_MAC
+    args.append(url);
+    process->start("open", args);
+#else
+#ifdef Q_OS_WIN32
+
+	QString pf(getenv("ProgramFiles"));
+	QString command = pf + QString("\\Internet Explorer\\IEXPLORE.EXE");
+
+	args.append(url);
+	process->start(command, args);
+
+#else
+#ifdef Q_WS_X11
+    if (!qgetenv("KDE_FULL_SESSION").isEmpty()) {
+        args.append("exec");
+        args.append(url);
+        process->start("kfmclient", args);
+    } else if (!qgetenv("BROWSER").isEmpty()) {
+        args.append(url);
+        process->start(qgetenv("BROWSER"), args);
+    } else {
+        args.append(url);
+        process->start("firefox", args);
+    }
+#endif
+#endif
+#endif
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/MainWindowBase.h	Wed Oct 24 16:37:58 2007 +0000
@@ -0,0 +1,339 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _MAIN_WINDOW_BASE_H_
+#define _MAIN_WINDOW_BASE_H_
+
+#include <QFrame>
+#include <QString>
+#include <QUrl>
+#include <QMainWindow>
+#include <QPointer>
+
+#include "base/Command.h"
+#include "view/ViewManager.h"
+#include "base/PropertyContainer.h"
+#include "base/RecentFiles.h"
+#include "layer/LayerFactory.h"
+#include "plugin/transform/Transform.h"
+#include "document/SVFileReader.h"
+#include "data/fileio/FileFinder.h"
+#include "data/fileio/FileSource.h"
+#include <map>
+
+class Document;
+class PaneStack;
+class Pane;
+class View;
+class Fader;
+class Overview;
+class Layer;
+class WaveformLayer;
+class WaveFileModel;
+class AudioCallbackPlaySource;
+class AudioCallbackPlayTarget;
+class CommandHistory;
+class QMenu;
+class AudioDial;
+class QLabel;
+class QCheckBox;
+class PreferencesDialog;
+class QTreeView;
+class QPushButton;
+class OSCQueue;
+class OSCMessage;
+class KeyReference;
+class Labeller;
+
+/**
+ * The base class for the SV main window.  This includes everything to
+ * do with general document and pane stack management, but nothing
+ * that involves user interaction -- this doesn't create the widget or
+ * menu structures or editing tools, and if a function needs to open a
+ * dialog, it shouldn't be in here.  This permits "variations on SV"
+ * to use different subclasses retaining the same general structure.
+ */
+
+class MainWindowBase : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindowBase(bool withAudioOutput, bool withOSCSupport);
+    virtual ~MainWindowBase();
+    
+    enum AudioFileOpenMode {
+        ReplaceMainModel,
+        CreateAdditionalModel,
+        ReplaceCurrentPane,
+        AskUser
+    };
+
+    enum FileOpenStatus {
+        FileOpenSucceeded,
+        FileOpenFailed,
+        FileOpenCancelled,
+        FileOpenWrongMode // attempted to open layer when no main model present
+    };
+
+    virtual FileOpenStatus open(QString fileOrUrl, AudioFileOpenMode = AskUser);
+    virtual FileOpenStatus open(FileSource source, AudioFileOpenMode = AskUser);
+    
+    virtual FileOpenStatus openAudio(FileSource source, AudioFileOpenMode = AskUser);
+    virtual FileOpenStatus openPlaylist(FileSource source, AudioFileOpenMode = AskUser);
+    virtual FileOpenStatus openLayer(FileSource source);
+    virtual FileOpenStatus openImage(FileSource source);
+
+    virtual FileOpenStatus openSessionFile(QString fileOrUrl);
+    virtual FileOpenStatus openSession(FileSource source);
+
+    virtual bool saveSessionFile(QString path);
+
+signals:
+    // Used to toggle the availability of menu actions
+    void canAddPane(bool);
+    void canDeleteCurrentPane(bool);
+    void canAddLayer(bool);
+    void canImportMoreAudio(bool);
+    void canImportLayer(bool);
+    void canExportAudio(bool);
+    void canExportLayer(bool);
+    void canExportImage(bool);
+    void canRenameLayer(bool);
+    void canEditLayer(bool);
+    void canMeasureLayer(bool);
+    void canSelect(bool);
+    void canClearSelection(bool);
+    void canEditSelection(bool);
+    void canDeleteSelection(bool);
+    void canPaste(bool);
+    void canInsertInstant(bool);
+    void canInsertInstantsAtBoundaries(bool);
+    void canRenumberInstants(bool);
+    void canDeleteCurrentLayer(bool);
+    void canZoom(bool);
+    void canScroll(bool);
+    void canPlay(bool);
+    void canFfwd(bool);
+    void canRewind(bool);
+    void canPlaySelection(bool);
+    void canSpeedUpPlayback(bool);
+    void canSlowDownPlayback(bool);
+    void canChangePlaybackSpeed(bool);
+    void canSave(bool);
+
+public slots:
+    virtual void preferenceChanged(PropertyContainer::PropertyName);
+
+protected slots:
+    virtual void zoomIn();
+    virtual void zoomOut();
+    virtual void zoomToFit();
+    virtual void zoomDefault();
+    virtual void scrollLeft();
+    virtual void scrollRight();
+    virtual void jumpLeft();
+    virtual void jumpRight();
+
+    virtual void showNoOverlays();
+    virtual void showMinimalOverlays();
+    virtual void showStandardOverlays();
+    virtual void showAllOverlays();
+
+    virtual void toggleZoomWheels();
+    virtual void togglePropertyBoxes();
+    virtual void toggleStatusBar();
+
+    virtual void play();
+    virtual void ffwd();
+    virtual void ffwdEnd();
+    virtual void rewind();
+    virtual void rewindStart();
+    virtual void stop();
+
+    virtual void deleteCurrentPane();
+    virtual void deleteCurrentLayer();
+
+    virtual void playLoopToggled();
+    virtual void playSelectionToggled();
+    virtual void playSoloToggled();
+
+    virtual void sampleRateMismatch(size_t, size_t, bool) = 0;
+    virtual void audioOverloadPluginDisabled() = 0;
+
+    virtual void playbackFrameChanged(unsigned long);
+    virtual void globalCentreFrameChanged(unsigned long);
+    virtual void viewCentreFrameChanged(View *, unsigned long);
+    virtual void viewZoomLevelChanged(View *, unsigned long, bool);
+    virtual void outputLevelsChanged(float, float) = 0;
+
+    virtual void currentPaneChanged(Pane *);
+    virtual void currentLayerChanged(Pane *, Layer *);
+
+    virtual void selectAll();
+    virtual void selectToStart();
+    virtual void selectToEnd();
+    virtual void selectVisible();
+    virtual void clearSelection();
+
+    virtual void cut();
+    virtual void copy();
+    virtual void paste();
+    virtual void deleteSelected();
+
+    virtual void insertInstant();
+    virtual void insertInstantAt(size_t);
+    virtual void insertInstantsAtBoundaries();
+    virtual void renumberInstants();
+
+    virtual void documentModified();
+    virtual void documentRestored();
+
+    virtual void layerAdded(Layer *);
+    virtual void layerRemoved(Layer *);
+    virtual void layerAboutToBeDeleted(Layer *);
+    virtual void layerInAView(Layer *, bool);
+
+    virtual void mainModelChanged(WaveFileModel *);
+    virtual void modelAdded(Model *);
+    virtual void modelAboutToBeDeleted(Model *);
+
+    virtual void updateMenuStates();
+    virtual void updateDescriptionLabel() = 0;
+
+    virtual void modelGenerationFailed(QString) = 0;
+    virtual void modelRegenerationFailed(QString, QString) = 0;
+
+    virtual void rightButtonMenuRequested(Pane *, QPoint point) = 0;
+
+    virtual void paneAdded(Pane *) = 0;
+    virtual void paneHidden(Pane *) = 0;
+    virtual void paneAboutToBeDeleted(Pane *) = 0;
+    virtual void paneDropAccepted(Pane *, QStringList) = 0;
+    virtual void paneDropAccepted(Pane *, QString) = 0;
+
+    virtual void pollOSC();
+    virtual void handleOSCMessage(const OSCMessage &) = 0;
+
+    virtual void contextHelpChanged(const QString &);
+    virtual void inProgressSelectionChanged();
+
+    virtual void closeSession() = 0;
+
+protected:
+    QString                  m_sessionFile;
+    QString                  m_audioFile;
+    Document                *m_document;
+
+    QLabel                  *m_descriptionLabel;
+    PaneStack               *m_paneStack;
+    ViewManager             *m_viewManager;
+    Layer                   *m_timeRulerLayer;
+
+    bool                     m_audioOutput;
+    AudioCallbackPlaySource *m_playSource;
+    AudioCallbackPlayTarget *m_playTarget;
+
+    OSCQueue                *m_oscQueue;
+
+    RecentFiles              m_recentFiles;
+    RecentFiles              m_recentTransforms;
+
+    bool                     m_documentModified;
+    bool                     m_openingAudioFile;
+    bool                     m_abandoning;
+
+    Labeller                *m_labeller;
+
+    int                      m_lastPlayStatusSec;
+    mutable QString          m_myStatusMessage;
+
+    bool                     m_initialDarkBackground;
+
+    WaveFileModel *getMainModel();
+    const WaveFileModel *getMainModel() const;
+    void createDocument();
+
+    Pane *addPaneToStack();
+    Layer *getSnapLayer() const;
+
+    class PaneCallback : public SVFileReaderPaneCallback
+    {
+    public:
+	PaneCallback(MainWindowBase *mw) : m_mw(mw) { }
+	virtual Pane *addPane() { return m_mw->addPaneToStack(); }
+	virtual void setWindowSize(int width, int height) {
+	    m_mw->resize(width, height);
+	}
+	virtual void addSelection(int start, int end) {
+	    m_mw->m_viewManager->addSelection(Selection(start, end));
+	}
+    protected:
+	MainWindowBase *m_mw;
+    };
+
+    class AddPaneCommand : public Command
+    {
+    public:
+	AddPaneCommand(MainWindowBase *mw);
+	virtual ~AddPaneCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const;
+
+	Pane *getPane() { return m_pane; }
+
+    protected:
+	MainWindowBase *m_mw;
+	Pane *m_pane; // Main window owns this, but I determine its lifespan
+	Pane *m_prevCurrentPane; // I don't own this
+	bool m_added;
+    };
+
+    class RemovePaneCommand : public Command
+    {
+    public:
+	RemovePaneCommand(MainWindowBase *mw, Pane *pane);
+	virtual ~RemovePaneCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const;
+
+    protected:
+	MainWindowBase *m_mw;
+	Pane *m_pane; // Main window owns this, but I determine its lifespan
+	Pane *m_prevCurrentPane; // I don't own this
+	bool m_added;
+    };
+
+    virtual bool checkSaveModified() = 0;
+
+    virtual QString getOpenFileName(FileFinder::FileType type);
+    virtual QString getSaveFileName(FileFinder::FileType type);
+    virtual void registerLastOpenedFilePath(FileFinder::FileType type, QString path);
+
+    virtual void createPlayTarget();
+    virtual void openHelpUrl(QString url);
+
+    virtual void setupMenus() = 0;
+    virtual void updateVisibleRangeDisplay(Pane *p) const = 0;
+
+    virtual void toXml(QTextStream &stream);
+};
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/SVFileReader.cpp	Wed Oct 24 16:37:58 2007 +0000
@@ -0,0 +1,1167 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "SVFileReader.h"
+
+#include "layer/Layer.h"
+#include "view/View.h"
+#include "base/PlayParameters.h"
+#include "base/PlayParameterRepository.h"
+
+#include "data/fileio/AudioFileReaderFactory.h"
+#include "data/fileio/FileFinder.h"
+#include "data/fileio/FileSource.h"
+
+#include "data/model/WaveFileModel.h"
+#include "data/model/EditableDenseThreeDimensionalModel.h"
+#include "data/model/SparseOneDimensionalModel.h"
+#include "data/model/SparseTimeValueModel.h"
+#include "data/model/NoteModel.h"
+#include "data/model/TextModel.h"
+#include "data/model/ImageModel.h"
+
+#include "view/Pane.h"
+
+#include "Document.h"
+
+#include <QString>
+#include <QMessageBox>
+#include <QFileDialog>
+
+#include <iostream>
+
+SVFileReader::SVFileReader(Document *document,
+			   SVFileReaderPaneCallback &callback,
+                           QString location) :
+    m_document(document),
+    m_paneCallback(callback),
+    m_location(location),
+    m_currentPane(0),
+    m_currentDataset(0),
+    m_currentDerivedModel(0),
+    m_currentDerivedModelId(-1),
+    m_currentPlayParameters(0),
+    m_datasetSeparator(" "),
+    m_inRow(false),
+    m_inLayer(false),
+    m_inView(false),
+    m_rowNumber(0),
+    m_ok(false)
+{
+}
+
+void
+SVFileReader::parse(const QString &xmlData)
+{
+    QXmlInputSource inputSource;
+    inputSource.setData(xmlData);
+    parse(inputSource);
+}
+
+void
+SVFileReader::parse(QXmlInputSource &inputSource)
+{
+    QXmlSimpleReader reader;
+    reader.setContentHandler(this);
+    reader.setErrorHandler(this);
+    m_ok = reader.parse(inputSource);
+}    
+
+bool
+SVFileReader::isOK()
+{
+    return m_ok;
+}
+	
+SVFileReader::~SVFileReader()
+{
+    if (!m_awaitingDatasets.empty()) {
+	std::cerr << "WARNING: SV-XML: File ended with "
+		  << m_awaitingDatasets.size() << " unfilled model dataset(s)"
+		  << std::endl;
+    }
+
+    std::set<Model *> unaddedModels;
+
+    for (std::map<int, Model *>::iterator i = m_models.begin();
+	 i != m_models.end(); ++i) {
+	if (m_addedModels.find(i->second) == m_addedModels.end()) {
+	    unaddedModels.insert(i->second);
+	}
+    }
+
+    if (!unaddedModels.empty()) {
+	std::cerr << "WARNING: SV-XML: File contained "
+		  << unaddedModels.size() << " unused models"
+		  << std::endl;
+	while (!unaddedModels.empty()) {
+	    delete *unaddedModels.begin();
+	    unaddedModels.erase(unaddedModels.begin());
+	}
+    }	
+}
+
+bool
+SVFileReader::startElement(const QString &, const QString &,
+			   const QString &qName,
+			   const QXmlAttributes &attributes)
+{
+    QString name = qName.toLower();
+
+    bool ok = false;
+
+    // Valid element names:
+    //
+    // sv
+    // data
+    // dataset
+    // display
+    // derivation
+    // playparameters
+    // layer
+    // model
+    // point
+    // row
+    // view
+    // window
+
+    if (name == "sv") {
+
+	// nothing needed
+	ok = true;
+
+    } else if (name == "data") {
+
+	// nothing needed
+	m_inData = true;
+	ok = true;
+
+    } else if (name == "display") {
+
+	// nothing needed
+	ok = true;
+
+    } else if (name == "window") {
+
+	ok = readWindow(attributes);
+
+    } else if (name == "model") {
+
+	ok = readModel(attributes);
+    
+    } else if (name == "dataset") {
+	
+	ok = readDatasetStart(attributes);
+
+    } else if (name == "bin") {
+	
+	ok = addBinToDataset(attributes);
+    
+    } else if (name == "point") {
+	
+	ok = addPointToDataset(attributes);
+
+    } else if (name == "row") {
+
+	ok = addRowToDataset(attributes);
+
+    } else if (name == "layer") {
+
+        addUnaddedModels(); // all models must be specified before first layer
+	ok = readLayer(attributes);
+
+    } else if (name == "view") {
+
+	m_inView = true;
+	ok = readView(attributes);
+
+    } else if (name == "derivation") {
+
+	ok = readDerivation(attributes);
+
+    } else if (name == "playparameters") {
+        
+        ok = readPlayParameters(attributes);
+
+    } else if (name == "plugin") {
+
+	ok = readPlugin(attributes);
+
+    } else if (name == "selections") {
+
+	m_inSelections = true;
+	ok = true;
+
+    } else if (name == "selection") {
+
+	ok = readSelection(attributes);
+
+    } else if (name == "measurement") {
+
+        ok = readMeasurement(attributes);
+
+    } else {
+        std::cerr << "WARNING: SV-XML: Unexpected element \""
+                  << name.toLocal8Bit().data() << "\"" << std::endl;
+    }
+
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: Failed to completely process element \""
+		  << name.toLocal8Bit().data() << "\"" << std::endl;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::characters(const QString &text)
+{
+    bool ok = false;
+
+    if (m_inRow) {
+	ok = readRowData(text);
+	if (!ok) {
+	    std::cerr << "WARNING: SV-XML: Failed to read row data content for row " << m_rowNumber << std::endl;
+	}
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::endElement(const QString &, const QString &,
+			 const QString &qName)
+{
+    QString name = qName.toLower();
+
+    if (name == "dataset") {
+
+	if (m_currentDataset) {
+	    
+	    bool foundInAwaiting = false;
+
+	    for (std::map<int, int>::iterator i = m_awaitingDatasets.begin();
+		 i != m_awaitingDatasets.end(); ++i) {
+		if (haveModel(i->second) &&
+                    m_models[i->second] == m_currentDataset) {
+		    m_awaitingDatasets.erase(i);
+		    foundInAwaiting = true;
+		    break;
+		}
+	    }
+
+	    if (!foundInAwaiting) {
+		std::cerr << "WARNING: SV-XML: Dataset precedes model, or no model uses dataset" << std::endl;
+	    }
+	}
+
+	m_currentDataset = 0;
+
+    } else if (name == "data") {
+
+        addUnaddedModels();
+	m_inData = false;
+
+    } else if (name == "derivation") {
+
+        if (!m_currentDerivedModel) {
+            if (m_currentDerivedModel < 0) {
+                std::cerr << "WARNING: SV-XML: Bad derivation output model id "
+                          << m_currentDerivedModelId << std::endl;
+            } else if (haveModel(m_currentDerivedModelId)) {
+                std::cerr << "WARNING: SV-XML: Derivation has existing model "
+                          << m_currentDerivedModelId
+                          << " as target, not regenerating" << std::endl;
+            } else {
+                m_currentDerivedModel = m_models[m_currentDerivedModelId] =
+                    m_document->addDerivedModel(m_currentTransform,
+                                                m_currentTransformSource,
+                                                m_currentTransformContext,
+                                                m_currentTransformConfiguration);
+            }
+        } else {
+            m_document->addDerivedModel(m_currentTransform,
+                                        m_currentTransformSource,
+                                        m_currentTransformContext,
+                                        m_currentDerivedModel,
+                                        m_currentTransformConfiguration);
+        }
+
+        m_addedModels.insert(m_currentDerivedModel);
+        m_currentDerivedModel = 0;
+        m_currentDerivedModelId = -1;
+        m_currentTransform = "";
+        m_currentTransformConfiguration = "";
+
+    } else if (name == "row") {
+	m_inRow = false;
+    } else if (name == "layer") {
+        m_inLayer = false;
+    } else if (name == "view") {
+	m_inView = false;
+    } else if (name == "selections") {
+	m_inSelections = false;
+    } else if (name == "playparameters") {
+        m_currentPlayParameters = 0;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::error(const QXmlParseException &exception)
+{
+    m_errorString =
+	QString("ERROR: SV-XML: %1 at line %2, column %3")
+	.arg(exception.message())
+	.arg(exception.lineNumber())
+	.arg(exception.columnNumber());
+    std::cerr << m_errorString.toLocal8Bit().data() << std::endl;
+    return QXmlDefaultHandler::error(exception);
+}
+
+bool
+SVFileReader::fatalError(const QXmlParseException &exception)
+{
+    m_errorString =
+	QString("FATAL ERROR: SV-XML: %1 at line %2, column %3")
+	.arg(exception.message())
+	.arg(exception.lineNumber())
+	.arg(exception.columnNumber());
+    std::cerr << m_errorString.toLocal8Bit().data() << std::endl;
+    return QXmlDefaultHandler::fatalError(exception);
+}
+
+
+#define READ_MANDATORY(TYPE, NAME, CONVERSION)		      \
+    TYPE NAME = attributes.value(#NAME).trimmed().CONVERSION(&ok); \
+    if (!ok) { \
+	std::cerr << "WARNING: SV-XML: Missing or invalid mandatory " #TYPE " attribute \"" #NAME "\"" << std::endl; \
+	return false; \
+    }
+
+bool
+SVFileReader::readWindow(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, width, toInt);
+    READ_MANDATORY(int, height, toInt);
+
+    m_paneCallback.setWindowSize(width, height);
+    return true;
+}
+
+void
+SVFileReader::addUnaddedModels()
+{
+    std::set<Model *> unaddedModels;
+    
+    for (std::map<int, Model *>::iterator i = m_models.begin();
+         i != m_models.end(); ++i) {
+        if (m_addedModels.find(i->second) == m_addedModels.end()) {
+            unaddedModels.insert(i->second);
+        }
+    }
+    
+    for (std::set<Model *>::iterator i = unaddedModels.begin();
+         i != unaddedModels.end(); ++i) {
+        m_document->addImportedModel(*i);
+        m_addedModels.insert(*i);
+    }
+}
+
+bool
+SVFileReader::readModel(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, id, toInt);
+
+    if (haveModel(id)) {
+	std::cerr << "WARNING: SV-XML: Ignoring duplicate model id " << id
+		  << std::endl;
+	return false;
+    }
+
+    QString name = attributes.value("name");
+
+    std::cerr << "SVFileReader::readModel: model name \"" << name.toStdString() << "\"" << std::endl;
+
+    READ_MANDATORY(int, sampleRate, toInt);
+
+    QString type = attributes.value("type").trimmed();
+    bool mainModel = (attributes.value("mainModel").trimmed() == "true");
+
+    if (type == "wavefile") {
+	
+        WaveFileModel *model = 0;
+        FileFinder *ff = FileFinder::getInstance();
+        QString originalPath = attributes.value("file");
+        QString path = ff->find(FileFinder::AudioFile,
+                                originalPath, m_location);
+
+        FileSource file(path);
+        file.waitForStatus();
+
+        if (!file.isOK()) {
+            std::cerr << "SVFileReader::readModel: Failed to retrieve file \"" << path.toStdString() << "\" for wave file model: " << file.getErrorString().toStdString() << std::endl;
+        } else if (!file.isAvailable()) {
+            std::cerr << "SVFileReader::readModel: Failed to retrieve file \"" << path.toStdString() << "\" for wave file model: Source unavailable" << std::endl;
+        } else {
+
+            file.waitForData();
+            model = new WaveFileModel(file);
+            if (!model->isOK()) {
+                delete model;
+                model = 0;
+            }
+        }
+
+        if (!model) return false;
+
+        model->setObjectName(name);
+	m_models[id] = model;
+	if (mainModel) {
+	    m_document->setMainModel(model);
+	    m_addedModels.insert(model);
+	}
+	// Derived models will be added when their derivation
+	// is found.
+
+	return true;
+
+    } else if (type == "dense") {
+	
+	READ_MANDATORY(int, dimensions, toInt);
+		    
+	// Currently the only dense model we support here is the dense
+	// 3d model.  Dense time-value models are always file-backed
+	// waveform data, at this point, and they come in as wavefile
+	// models.
+	
+	if (dimensions == 3) {
+	    
+	    READ_MANDATORY(int, windowSize, toInt);
+	    READ_MANDATORY(int, yBinCount, toInt);
+	    
+            EditableDenseThreeDimensionalModel *model =
+		new EditableDenseThreeDimensionalModel
+                (sampleRate, windowSize, yBinCount);
+	    
+	    float minimum = attributes.value("minimum").trimmed().toFloat(&ok);
+	    if (ok) model->setMinimumLevel(minimum);
+	    
+	    float maximum = attributes.value("maximum").trimmed().toFloat(&ok);
+	    if (ok) model->setMaximumLevel(maximum);
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+            model->setObjectName(name);
+	    m_models[id] = model;
+	    return true;
+
+	} else {
+
+	    std::cerr << "WARNING: SV-XML: Unexpected dense model dimension ("
+		      << dimensions << ")" << std::endl;
+	}
+    } else if (type == "sparse") {
+
+	READ_MANDATORY(int, dimensions, toInt);
+		  
+	if (dimensions == 1) {
+	    
+	    READ_MANDATORY(int, resolution, toInt);
+
+            if (attributes.value("subtype") == "image") {
+
+                bool notifyOnAdd = (attributes.value("notifyOnAdd") == "true");
+                ImageModel *model = new ImageModel(sampleRate, resolution,
+                                                   notifyOnAdd);
+                model->setObjectName(name);
+                m_models[id] = model;
+
+            } else {
+
+                SparseOneDimensionalModel *model = new SparseOneDimensionalModel
+                    (sampleRate, resolution);
+                model->setObjectName(name);
+                m_models[id] = model;
+            }
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    return true;
+
+	} else if (dimensions == 2 || dimensions == 3) {
+	    
+	    READ_MANDATORY(int, resolution, toInt);
+
+            bool haveMinMax = true;
+	    float minimum = attributes.value("minimum").trimmed().toFloat(&ok);
+            if (!ok) haveMinMax = false;
+	    float maximum = attributes.value("maximum").trimmed().toFloat(&ok);
+            if (!ok) haveMinMax = false;
+
+	    float valueQuantization =
+		attributes.value("valueQuantization").trimmed().toFloat(&ok);
+
+	    bool notifyOnAdd = (attributes.value("notifyOnAdd") == "true");
+
+            QString units = attributes.value("units");
+
+	    if (dimensions == 2) {
+		if (attributes.value("subtype") == "text") {
+		    TextModel *model = new TextModel
+			(sampleRate, resolution, notifyOnAdd);
+                    model->setObjectName(name);
+		    m_models[id] = model;
+		} else {
+		    SparseTimeValueModel *model;
+                    if (haveMinMax) {
+                        model = new SparseTimeValueModel
+                            (sampleRate, resolution, minimum, maximum, notifyOnAdd);
+                    } else {
+                        model = new SparseTimeValueModel
+                            (sampleRate, resolution, notifyOnAdd);
+                    }
+                    model->setScaleUnits(units);
+                    model->setObjectName(name);
+		    m_models[id] = model;
+		}
+	    } else {
+		NoteModel *model;
+                if (haveMinMax) {
+                    model = new NoteModel
+                        (sampleRate, resolution, minimum, maximum, notifyOnAdd);
+                } else {
+                    model = new NoteModel
+                        (sampleRate, resolution, notifyOnAdd);
+                }
+		model->setValueQuantization(valueQuantization);
+                model->setScaleUnits(units);
+                model->setObjectName(name);
+		m_models[id] = model;
+	    }
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    return true;
+
+	} else {
+
+	    std::cerr << "WARNING: SV-XML: Unexpected sparse model dimension ("
+		      << dimensions << ")" << std::endl;
+	}
+    } else {
+
+	std::cerr << "WARNING: SV-XML: Unexpected model type \""
+		  << type.toLocal8Bit().data() << "\" for model id " << id << std::endl;
+    }
+
+    return false;
+}
+
+bool
+SVFileReader::readView(const QXmlAttributes &attributes)
+{
+    QString type = attributes.value("type");
+    m_currentPane = 0;
+    
+    if (type != "pane") {
+	std::cerr << "WARNING: SV-XML: Unexpected view type \""
+		  << type.toLocal8Bit().data() << "\"" << std::endl;
+	return false;
+    }
+
+    m_currentPane = m_paneCallback.addPane();
+
+    if (!m_currentPane) {
+	std::cerr << "WARNING: SV-XML: Internal error: Failed to add pane!"
+		  << std::endl;
+	return false;
+    }
+
+    bool ok = false;
+
+    View *view = m_currentPane;
+
+    // The view properties first
+
+    READ_MANDATORY(size_t, centre, toUInt);
+    READ_MANDATORY(size_t, zoom, toUInt);
+    READ_MANDATORY(int, followPan, toInt);
+    READ_MANDATORY(int, followZoom, toInt);
+    QString tracking = attributes.value("tracking");
+
+    // Specify the follow modes before we set the actual values
+    view->setFollowGlobalPan(followPan);
+    view->setFollowGlobalZoom(followZoom);
+    view->setPlaybackFollow(tracking == "scroll" ? PlaybackScrollContinuous :
+			    tracking == "page" ? PlaybackScrollPage
+			    : PlaybackIgnore);
+
+    // Then set these values
+    view->setCentreFrame(centre);
+    view->setZoomLevel(zoom);
+
+    // And pane properties
+    READ_MANDATORY(int, centreLineVisible, toInt);
+    m_currentPane->setCentreLineVisible(centreLineVisible);
+
+    int height = attributes.value("height").toInt(&ok);
+    if (ok) {
+	m_currentPane->resize(m_currentPane->width(), height);
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readLayer(const QXmlAttributes &attributes)
+{
+    QString type = attributes.value("type");
+
+    int id;
+    bool ok = false;
+    id = attributes.value("id").trimmed().toInt(&ok);
+
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: No layer id for layer of type \""
+		  << type.toLocal8Bit().data()
+		  << "\"" << std::endl;
+	return false;
+    }
+
+    Layer *layer = 0;
+    bool isNewLayer = false;
+
+    // Layers are expected to be defined in layer elements in the data
+    // section, and referred to in layer elements in the view
+    // sections.  So if we're in the data section, we expect this
+    // layer not to exist already; if we're in the view section, we
+    // expect it to exist.
+
+    if (m_inData) {
+
+	if (m_layers.find(id) != m_layers.end()) {
+	    std::cerr << "WARNING: SV-XML: Ignoring duplicate layer id " << id
+		      << " in data section" << std::endl;
+	    return false;
+	}
+
+	layer = m_layers[id] = m_document->createLayer
+	    (LayerFactory::getInstance()->getLayerTypeForName(type));
+
+	if (layer) {
+	    m_layers[id] = layer;
+	    isNewLayer = true;
+	}
+
+    } else {
+
+	if (!m_currentPane) {
+	    std::cerr << "WARNING: SV-XML: No current pane for layer " << id
+		      << " in view section" << std::endl;
+	    return false;
+	}
+
+	if (m_layers.find(id) != m_layers.end()) {
+	    
+	    layer = m_layers[id];
+	
+	} else {
+	    std::cerr << "WARNING: SV-XML: Layer id " << id 
+		      << " in view section has not been defined -- defining it here"
+		      << std::endl;
+
+	    layer = m_document->createLayer
+		(LayerFactory::getInstance()->getLayerTypeForName(type));
+
+	    if (layer) {
+		m_layers[id] = layer;
+		isNewLayer = true;
+	    }
+	}
+    }
+	    
+    if (!layer) {
+	std::cerr << "WARNING: SV-XML: Failed to add layer of type \""
+		  << type.toLocal8Bit().data()
+		  << "\"" << std::endl;
+	return false;
+    }
+
+    if (isNewLayer) {
+
+	QString name = attributes.value("name");
+	layer->setObjectName(name);
+
+	int modelId;
+	bool modelOk = false;
+	modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+	if (modelOk) {
+	    if (haveModel(modelId)) {
+		Model *model = m_models[modelId];
+		m_document->setModel(layer, model);
+	    } else {
+		std::cerr << "WARNING: SV-XML: Unknown model id " << modelId
+			  << " in layer definition" << std::endl;
+	    }
+	}
+
+	layer->setProperties(attributes);
+    }
+
+    if (!m_inData && m_currentPane) {
+
+        QString visible = attributes.value("visible");
+        bool dormant = (visible == "false");
+
+        // We need to do this both before and after adding the layer
+        // to the view -- we need it to be dormant if appropriate
+        // before it's actually added to the view so that any property
+        // box gets the right state when it's added, but the add layer
+        // command sets dormant to false because it assumes it may be
+        // restoring a previously dormant layer, so we need to set it
+        // again afterwards too.  Hm
+        layer->setLayerDormant(m_currentPane, dormant);
+
+	m_document->addLayerToView(m_currentPane, layer);
+
+        layer->setLayerDormant(m_currentPane, dormant);
+    }
+
+    m_currentLayer = layer;
+    m_inLayer = true;
+
+    return true;
+}
+
+bool
+SVFileReader::readDatasetStart(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, id, toInt);
+    READ_MANDATORY(int, dimensions, toInt);
+    
+    if (m_awaitingDatasets.find(id) == m_awaitingDatasets.end()) {
+	std::cerr << "WARNING: SV-XML: Unwanted dataset " << id << std::endl;
+	return false;
+    }
+    
+    int modelId = m_awaitingDatasets[id];
+    
+    Model *model = 0;
+    if (haveModel(modelId)) {
+	model = m_models[modelId];
+    } else {
+	std::cerr << "WARNING: SV-XML: Internal error: Unknown model " << modelId
+		  << " expecting dataset " << id << std::endl;
+	return false;
+    }
+
+    bool good = false;
+
+    switch (dimensions) {
+    case 1:
+	if (dynamic_cast<SparseOneDimensionalModel *>(model)) good = true;
+        else if (dynamic_cast<ImageModel *>(model)) good = true;
+	break;
+
+    case 2:
+	if (dynamic_cast<SparseTimeValueModel *>(model)) good = true;
+	else if (dynamic_cast<TextModel *>(model)) good = true;
+	break;
+
+    case 3:
+	if (dynamic_cast<NoteModel *>(model)) good = true;
+	else if (dynamic_cast<EditableDenseThreeDimensionalModel *>(model)) {
+	    m_datasetSeparator = attributes.value("separator");
+	    good = true;
+	}
+	break;
+    }
+
+    if (!good) {
+	std::cerr << "WARNING: SV-XML: Model id " << modelId << " has wrong number of dimensions or inappropriate type for " << dimensions << "-D dataset " << id << std::endl;
+	m_currentDataset = 0;
+	return false;
+    }
+
+    m_currentDataset = model;
+    return true;
+}
+
+bool
+SVFileReader::addPointToDataset(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, frame, toInt);
+
+//    std::cerr << "SVFileReader::addPointToDataset: frame = " << frame << std::endl;
+
+    SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
+	(m_currentDataset);
+
+    if (sodm) {
+//        std::cerr << "Current dataset is a sparse one dimensional model" << std::endl;
+	QString label = attributes.value("label");
+	sodm->addPoint(SparseOneDimensionalModel::Point(frame, label));
+	return true;
+    }
+
+    SparseTimeValueModel *stvm = dynamic_cast<SparseTimeValueModel *>
+	(m_currentDataset);
+
+    if (stvm) {
+//        std::cerr << "Current dataset is a sparse time-value model" << std::endl;
+	float value = 0.0;
+	value = attributes.value("value").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+	stvm->addPoint(SparseTimeValueModel::Point(frame, value, label));
+	return ok;
+    }
+	
+    NoteModel *nm = dynamic_cast<NoteModel *>(m_currentDataset);
+
+    if (nm) {
+//        std::cerr << "Current dataset is a note model" << std::endl;
+	float value = 0.0;
+	value = attributes.value("value").trimmed().toFloat(&ok);
+	size_t duration = 0;
+	duration = attributes.value("duration").trimmed().toUInt(&ok);
+	QString label = attributes.value("label");
+	nm->addPoint(NoteModel::Point(frame, value, duration, label));
+	return ok;
+    }
+
+    TextModel *tm = dynamic_cast<TextModel *>(m_currentDataset);
+
+    if (tm) {
+//        std::cerr << "Current dataset is a text model" << std::endl;
+	float height = 0.0;
+	height = attributes.value("height").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+//        std::cerr << "SVFileReader::addPointToDataset: TextModel: frame = " << frame << ", height = " << height << ", label = " << label.toStdString() << ", ok = " << ok << std::endl;
+	tm->addPoint(TextModel::Point(frame, height, label));
+	return ok;
+    }
+
+    ImageModel *im = dynamic_cast<ImageModel *>(m_currentDataset);
+
+    if (im) {
+//        std::cerr << "Current dataset is an image model" << std::endl;
+	QString image = attributes.value("image");
+	QString label = attributes.value("label");
+//        std::cerr << "SVFileReader::addPointToDataset: ImageModel: frame = " << frame << ", image = " << image.toStdString() << ", label = " << label.toStdString() << ", ok = " << ok << std::endl;
+	im->addPoint(ImageModel::Point(frame, image, label));
+	return ok;
+    }
+
+    std::cerr << "WARNING: SV-XML: Point element found in non-point dataset" << std::endl;
+
+    return false;
+}
+
+bool
+SVFileReader::addBinToDataset(const QXmlAttributes &attributes)
+{
+    EditableDenseThreeDimensionalModel *dtdm = 
+        dynamic_cast<EditableDenseThreeDimensionalModel *>
+	(m_currentDataset);
+
+    if (dtdm) {
+
+	bool ok = false;
+	int n = attributes.value("number").trimmed().toInt(&ok);
+	if (!ok) {
+	    std::cerr << "WARNING: SV-XML: Missing or invalid bin number"
+		      << std::endl;
+	    return false;
+	}
+
+	QString name = attributes.value("name");
+
+	dtdm->setBinName(n, name);
+	return true;
+    }
+
+    std::cerr << "WARNING: SV-XML: Bin definition found in incompatible dataset" << std::endl;
+
+    return false;
+}
+
+
+bool
+SVFileReader::addRowToDataset(const QXmlAttributes &attributes)
+{
+    m_inRow = false;
+
+    bool ok = false;
+    m_rowNumber = attributes.value("n").trimmed().toInt(&ok);
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: Missing or invalid row number"
+		  << std::endl;
+	return false;
+    }
+    
+    m_inRow = true;
+
+//    std::cerr << "SV-XML: In row " << m_rowNumber << std::endl;
+    
+    return true;
+}
+
+bool
+SVFileReader::readRowData(const QString &text)
+{
+    EditableDenseThreeDimensionalModel *dtdm =
+        dynamic_cast<EditableDenseThreeDimensionalModel *>
+	(m_currentDataset);
+
+    bool warned = false;
+
+    if (dtdm) {
+	QStringList data = text.split(m_datasetSeparator);
+
+	DenseThreeDimensionalModel::Column values;
+
+	for (QStringList::iterator i = data.begin(); i != data.end(); ++i) {
+
+	    if (values.size() == dtdm->getHeight()) {
+		if (!warned) {
+		    std::cerr << "WARNING: SV-XML: Too many y-bins in 3-D dataset row "
+			      << m_rowNumber << std::endl;
+		    warned = true;
+		}
+	    }
+
+	    bool ok;
+	    float value = i->toFloat(&ok);
+	    if (!ok) {
+		std::cerr << "WARNING: SV-XML: Bad floating-point value "
+			  << i->toLocal8Bit().data()
+			  << " in row data" << std::endl;
+	    } else {
+		values.push_back(value);
+	    }
+	}
+
+	dtdm->setColumn(m_rowNumber, values);
+	return true;
+    }
+
+    std::cerr << "WARNING: SV-XML: Row data found in non-row dataset" << std::endl;
+
+    return false;
+}
+
+bool
+SVFileReader::readDerivation(const QXmlAttributes &attributes)
+{
+    int modelId = 0;
+    bool modelOk = false;
+    modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+    if (!modelOk) {
+	std::cerr << "WARNING: SV-XML: No model id specified for derivation" << std::endl;
+	return false;
+    }
+
+    QString transform = attributes.value("transform");
+
+    if (haveModel(modelId)) {
+        m_currentDerivedModel = m_models[modelId];
+    } else {
+        // we'll regenerate the model when the derivation element ends
+        m_currentDerivedModel = 0;
+    }
+    
+    m_currentDerivedModelId = modelId;
+    
+    int sourceId = 0;
+    bool sourceOk = false;
+    sourceId = attributes.value("source").trimmed().toInt(&sourceOk);
+
+    if (sourceOk && haveModel(sourceId)) {
+        m_currentTransformSource = m_models[sourceId];
+    } else {
+        m_currentTransformSource = m_document->getMainModel();
+    }
+
+    m_currentTransform = transform;
+    m_currentTransformConfiguration = "";
+
+    m_currentTransformContext = PluginTransform::ExecutionContext();
+
+    bool ok = false;
+    int channel = attributes.value("channel").trimmed().toInt(&ok);
+    if (ok) m_currentTransformContext.channel = channel;
+
+    int domain = attributes.value("domain").trimmed().toInt(&ok);
+    if (ok) m_currentTransformContext.domain = Vamp::Plugin::InputDomain(domain);
+
+    int stepSize = attributes.value("stepSize").trimmed().toInt(&ok);
+    if (ok) m_currentTransformContext.stepSize = stepSize;
+
+    int blockSize = attributes.value("blockSize").trimmed().toInt(&ok);
+    if (ok) m_currentTransformContext.blockSize = blockSize;
+
+    int windowType = attributes.value("windowType").trimmed().toInt(&ok);
+    if (ok) m_currentTransformContext.windowType = WindowType(windowType);
+
+    QString startFrameStr = attributes.value("startFrame");
+    QString durationStr = attributes.value("duration");
+
+    size_t startFrame = 0;
+    size_t duration = 0;
+
+    if (startFrameStr != "") {
+        startFrame = startFrameStr.trimmed().toInt(&ok);
+        if (!ok) startFrame = 0;
+    }
+    if (durationStr != "") {
+        duration = durationStr.trimmed().toInt(&ok);
+        if (!ok) duration = 0;
+    }
+
+    m_currentTransformContext.startFrame = startFrame;
+    m_currentTransformContext.duration = duration;
+
+    return true;
+}
+
+bool
+SVFileReader::readPlayParameters(const QXmlAttributes &attributes)
+{
+    m_currentPlayParameters = 0;
+
+    int modelId = 0;
+    bool modelOk = false;
+    modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+    if (!modelOk) {
+	std::cerr << "WARNING: SV-XML: No model id specified for play parameters" << std::endl;
+	return false;
+    }
+
+    if (haveModel(modelId)) {
+
+        bool ok = false;
+
+        PlayParameters *parameters = PlayParameterRepository::getInstance()->
+            getPlayParameters(m_models[modelId]);
+
+        if (!parameters) {
+            std::cerr << "WARNING: SV-XML: Play parameters for model "
+                      << modelId
+                      << " not found - has model been added to document?"
+                      << std::endl;
+            return false;
+        }
+        
+        bool muted = (attributes.value("mute").trimmed() == "true");
+        parameters->setPlayMuted(muted);
+        
+        float pan = attributes.value("pan").toFloat(&ok);
+        if (ok) parameters->setPlayPan(pan);
+        
+        float gain = attributes.value("gain").toFloat(&ok);
+        if (ok) parameters->setPlayGain(gain);
+        
+        QString pluginId = attributes.value("pluginId");
+        if (pluginId != "") parameters->setPlayPluginId(pluginId);
+        
+        m_currentPlayParameters = parameters;
+
+//        std::cerr << "Current play parameters for model: " << m_models[modelId] << ": " << m_currentPlayParameters << std::endl;
+
+    } else {
+
+	std::cerr << "WARNING: SV-XML: Unknown model " << modelId
+		  << " for play parameters" << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readPlugin(const QXmlAttributes &attributes)
+{
+    if (m_currentDerivedModelId < 0 && !m_currentPlayParameters) {
+        std::cerr << "WARNING: SV-XML: Plugin found outside derivation or play parameters" << std::endl;
+        return false;
+    }
+
+    QString configurationXml = "<plugin";
+    
+    for (int i = 0; i < attributes.length(); ++i) {
+        configurationXml += QString(" %1=\"%2\"")
+            .arg(attributes.qName(i))
+            .arg(XmlExportable::encodeEntities(attributes.value(i)));
+    }
+
+    configurationXml += "/>";
+
+    if (m_currentPlayParameters) {
+        m_currentPlayParameters->setPlayPluginConfiguration(configurationXml);
+    } else {
+        m_currentTransformConfiguration += configurationXml;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readSelection(const QXmlAttributes &attributes)
+{
+    bool ok;
+
+    READ_MANDATORY(int, start, toInt);
+    READ_MANDATORY(int, end, toInt);
+
+    m_paneCallback.addSelection(start, end);
+
+    return true;
+}
+
+bool
+SVFileReader::readMeasurement(const QXmlAttributes &attributes)
+{
+    std::cerr << "SVFileReader::readMeasurement: inLayer "
+              << m_inLayer << ", layer " << m_currentLayer << std::endl;
+
+    if (!m_inLayer) {
+        std::cerr << "WARNING: SV-XML: Measurement found outside layer" << std::endl;
+        return false;
+    }
+
+    m_currentLayer->addMeasurementRect(attributes);
+    return true;
+}
+
+SVFileReaderPaneCallback::~SVFileReaderPaneCallback()
+{
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/SVFileReader.h	Wed Oct 24 16:37:58 2007 +0000
@@ -0,0 +1,234 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _SV_FILE_READER_H_
+#define _SV_FILE_READER_H_
+
+#include "layer/LayerFactory.h"
+#include "plugin/transform/Transform.h"
+#include "plugin/transform/PluginTransform.h"
+
+#include <QXmlDefaultHandler>
+
+#include <map>
+
+class Pane;
+class Model;
+class Document;
+class PlayParameters;
+
+class SVFileReaderPaneCallback
+{
+public:
+    virtual ~SVFileReaderPaneCallback();
+    virtual Pane *addPane() = 0;
+    virtual void setWindowSize(int width, int height) = 0;
+    virtual void addSelection(int start, int end) = 0;
+};
+
+/**
+    SVFileReader loads Sonic Visualiser XML files.  (The SV file
+    format is bzipped XML.)
+
+    Some notes about the SV XML format follow.  We're very lazy with
+    our XML: there's no schema or DTD, and we depend heavily on
+    elements being in a particular order.
+ 
+\verbatim
+
+    <sv>
+
+    <data>
+
+      <!-- The data section contains definitions of both models and
+           visual layers.  Layers are considered data in the document;
+           the structure of views that displays the layers is not. -->
+
+      <!-- id numbers are unique within the data type (i.e. no two
+           models can have the same id, but a model can have the same
+           id as a layer, etc).  SV generates its id numbers just for
+           the purpose of cross-referencing within the current file;
+           they don't necessarily have any meaning once the file has
+           been loaded. -->
+
+      <model id="0" name="..." type="..." ... />
+      <model id="1" name="..." type="..." ... />
+
+      <!-- Models that have data associated with them store it
+           in a neighbouring dataset element.  The dataset must follow
+           the model and precede any derivation or layer elements that
+           refer to the model. -->
+
+      <model id="2" name="..." type="..." dataset="0" ... />
+
+      <dataset id="0" type="..."> 
+        <point frame="..." value="..." ... />
+      </dataset>
+
+      <!-- Where one model is derived from another via a transform,
+           it has an associated derivation element.  This must follow
+           both the source and target model elements.  The source and
+           model attributes give the source model id and target model
+           id respectively.  A model can have both dataset and
+           derivation elements; if it does, dataset must appear first. 
+           If the model's data are not stored, but instead the model
+           is to be regenerated completely from the transform when 
+           the session is reloaded, then the model should have _only_
+           a derivation element, and no model element should appear
+           for it at all. -->
+
+      <derivation source="0" model="2" transform="..." ...>
+        <plugin id="..." ... />
+      </derivation>
+
+      <!-- The playparameters element lists playback settings for
+           a model. -->
+
+      <playparameters mute="false" pan="0" gain="1" model="1" ... />
+
+      <!-- Layer elements.  The models must have already been defined.
+           The same model may appear in more than one layer (of more
+           than one type). -->
+
+      <layer id="1" type="..." name="..." model="0" ... />
+      <layer id="2" type="..." name="..." model="1" ... />
+
+    </data>
+
+
+    <display>
+
+      <!-- The display element contains visual structure for the
+           layers.  It's simpler than the data section. -->
+
+      <!-- Overall preferred window size for this session. -->
+
+      <window width="..." height="..."/>
+
+      <!-- List of view elements to stack up.  Each one contains
+           a list of layers in stacking order, back to front. -->
+
+      <view type="pane" ...>
+        <layer id="1"/>
+        <layer id="2"/>
+      </view>
+
+      <!-- The layer elements just refer to layers defined in the
+           data section, so they don't have to have any attributes
+           other than the id.  For sort-of-historical reasons SV
+           actually does repeat the other attributes here, but
+           it doesn't need to. -->
+
+      <view type="pane" ...>
+        <layer id="2"/>
+      <view>
+
+    </display>
+
+
+    <!-- List of selected regions by audio frame extents. -->
+
+    <selections>
+      <selection start="..." end="..."/>
+    </selections>
+
+
+    </sv>
+ 
+\endverbatim
+ */
+
+
+class SVFileReader : public QXmlDefaultHandler
+{
+public:
+    SVFileReader(Document *document,
+		 SVFileReaderPaneCallback &callback,
+                 QString location = ""); // for audio file locate mechanism
+    virtual ~SVFileReader();
+
+    void parse(const QString &xmlData);
+    void parse(QXmlInputSource &source);
+
+    bool isOK();
+    QString getErrorString() const { return m_errorString; }
+
+    // For loading a single layer onto an existing pane
+    void setCurrentPane(Pane *pane) { m_currentPane = pane; }
+    
+    virtual bool startElement(const QString &namespaceURI,
+			      const QString &localName,
+			      const QString &qName,
+			      const QXmlAttributes& atts);
+
+    virtual bool characters(const QString &);
+
+    virtual bool endElement(const QString &namespaceURI,
+			    const QString &localName,
+			    const QString &qName);
+
+    bool error(const QXmlParseException &exception);
+    bool fatalError(const QXmlParseException &exception);
+
+protected:
+    bool readWindow(const QXmlAttributes &);
+    bool readModel(const QXmlAttributes &);
+    bool readView(const QXmlAttributes &);
+    bool readLayer(const QXmlAttributes &);
+    bool readDatasetStart(const QXmlAttributes &);
+    bool addBinToDataset(const QXmlAttributes &);
+    bool addPointToDataset(const QXmlAttributes &);
+    bool addRowToDataset(const QXmlAttributes &);
+    bool readRowData(const QString &);
+    bool readDerivation(const QXmlAttributes &);
+    bool readPlayParameters(const QXmlAttributes &);
+    bool readPlugin(const QXmlAttributes &);
+    bool readSelection(const QXmlAttributes &);
+    bool readMeasurement(const QXmlAttributes &);
+    void addUnaddedModels();
+
+    bool haveModel(int id) {
+        return (m_models.find(id) != m_models.end()) && m_models[id];
+    }
+
+    Document *m_document;
+    SVFileReaderPaneCallback &m_paneCallback;
+    QString m_location;
+    Pane *m_currentPane;
+    std::map<int, Layer *> m_layers;
+    std::map<int, Model *> m_models;
+    std::set<Model *> m_addedModels;
+    std::map<int, int> m_awaitingDatasets; // map dataset id -> model id
+    Layer *m_currentLayer;
+    Model *m_currentDataset;
+    Model *m_currentDerivedModel;
+    int m_currentDerivedModelId;
+    PlayParameters *m_currentPlayParameters;
+    QString m_currentTransform;
+    Model *m_currentTransformSource;
+    PluginTransform::ExecutionContext m_currentTransformContext;
+    QString m_currentTransformConfiguration;
+    QString m_datasetSeparator;
+    bool m_inRow;
+    bool m_inLayer;
+    bool m_inView;
+    bool m_inData;
+    bool m_inSelections;
+    int m_rowNumber;
+    QString m_errorString;
+    bool m_ok;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/document.pro	Wed Oct 24 16:37:58 2007 +0000
@@ -0,0 +1,23 @@
+TEMPLATE = lib
+
+SV_UNIT_PACKAGES = 
+load(../sv.prf)
+
+CONFIG += sv staticlib qt thread warn_on stl rtti exceptions
+QT += xml
+
+TARGET = svdocument
+
+DEPENDPATH += ..
+INCLUDEPATH += . ..
+OBJECTS_DIR = tmp_obj
+MOC_DIR = tmp_moc
+
+HEADERS += Document.h \
+           MainWindowBase.h \
+           SVFileReader.h
+
+SOURCES += Document.cpp \
+           MainWindowBase.cpp \
+           SVFileReader.cpp
+