view framework/MainWindowBase.cpp @ 570:6f54789f3127 3.0-integration

Fix race condition in first-time recording, where adding the recording wave model would prompt the audio play source to note that its channel count had increased (from 0 to, say, 2) and thus to cause the audio device to be reopened, stopping recording. Fix is to make this only happen if channel count increases beyond that of the device, which shouldn't happen in the recording case
author Chris Cannam
date Wed, 04 Jan 2017 11:48:03 +0000
parents 1cc23cee4ebf
children 5369359351cb
line wrap: on
line source
/* -*- 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.h"

#include "view/Pane.h"
#include "view/PaneStack.h"
#include "data/model/ReadOnlyWaveFileModel.h"
#include "data/model/WritableWaveFileModel.h"
#include "data/model/SparseOneDimensionalModel.h"
#include "data/model/NoteModel.h"
#include "data/model/FlexiNoteModel.h"
#include "data/model/Labeller.h"
#include "data/model/TabularModel.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 "layer/NoteLayer.h"
#include "layer/FlexiNoteLayer.h"
#include "layer/RegionLayer.h"

#include "widgets/ListInputDialog.h"
#include "widgets/CommandHistory.h"
#include "widgets/ProgressDialog.h"
#include "widgets/MIDIFileImportDialog.h"
#include "widgets/CSVFormatDialog.h"
#include "widgets/ModelDataTableDialog.h"
#include "widgets/InteractiveFileFinder.h"

#include "audio/AudioCallbackPlaySource.h"
#include "audio/AudioRecordTarget.h"
#include "audio/PlaySpeedRangeMapper.h"

#include "data/fileio/DataFileReaderFactory.h"
#include "data/fileio/PlaylistFileReader.h"
#include "data/fileio/WavFileWriter.h"
#include "data/fileio/MIDIFileWriter.h"
#include "data/fileio/BZipFileDevice.h"
#include "data/fileio/FileSource.h"
#include "data/fileio/AudioFileReaderFactory.h"
#include "rdf/RDFImporter.h"

#include "base/RecentFiles.h"

#include "base/PlayParameterRepository.h"
#include "base/XmlExportable.h"
#include "base/Profiler.h"
#include "base/Preferences.h"
#include "base/TempWriteFile.h"
#include "base/Exceptions.h"
#include "base/ResourceFinder.h"

#include "data/osc/OSCQueue.h"
#include "data/midi/MIDIInput.h"

#include <bqaudioio/SystemPlaybackTarget.h>
#include <bqaudioio/SystemAudioIO.h>
#include <bqaudioio/AudioFactory.h>
#include <bqaudioio/ResamplerWrapper.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 <QTextCodec>
#include <QProcess>
#include <QShortcut>
#include <QSettings>
#include <QDateTime>
#include <QProcess>
#include <QCheckBox>
#include <QRegExp>
#include <QScrollArea>
#include <QDesktopWidget>
#include <QSignalMapper>

#include <iostream>
#include <cstdio>
#include <errno.h>

using std::vector;
using std::map;
using std::set;

#ifdef Q_WS_X11
#define Window X11Window
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/SM/SMlib.h>

static int handle_x11_error(Display *dpy, XErrorEvent *err)
{
    char errstr[256];
    XGetErrorText(dpy, err->error_code, errstr, 256);
    if (err->error_code != BadWindow) {
	cerr << "Sonic Visualiser: X Error: "
		  << errstr << " " << int(err->error_code)
		  << "\nin major opcode:  "
		  << int(err->request_code) << endl;
    }
    return 0;
}
#undef Window
#endif

MainWindowBase::MainWindowBase(SoundOptions options) :
    m_document(0),
    m_paneStack(0),
    m_viewManager(0),
    m_timeRulerLayer(0),
    m_soundOptions(options),
    m_playSource(0),
    m_recordTarget(0),
    m_resamplerWrapper(0),
    m_playTarget(0),
    m_audioIO(0),
    m_oscQueue(0),
    m_oscQueueStarter(0),
    m_midiInput(0),
    m_recentFiles("RecentFiles", 20),
    m_recentTransforms("RecentTransforms", 20),
    m_documentModified(false),
    m_openingAudioFile(false),
    m_abandoning(false),
    m_labeller(0),
    m_lastPlayStatusSec(0),
    m_initialDarkBackground(false),
    m_defaultFfwdRwdStep(2, 0),
    m_audioRecordMode(RecordCreateAdditionalModel),
    m_statusLabel(0),
    m_iconsVisibleInMenus(true),
    m_menuShortcutMapper(0)
{
    Profiler profiler("MainWindowBase::MainWindowBase");

    if (options & WithAudioInput) {
        if (!(options & WithAudioOutput)) {
            cerr << "WARNING: MainWindowBase: WithAudioInput requires WithAudioOutput -- recording will not work" << endl;
        }
    }
    
    qRegisterMetaType<sv_frame_t>("sv_frame_t");
    qRegisterMetaType<sv_samplerate_t>("sv_samplerate_t");

#ifdef Q_WS_X11
    XSetErrorHandler(handle_x11_error);
#endif

    connect(this, SIGNAL(hideSplash()), this, SLOT(emitHideSplash()));
    
    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()));

    // set a sensible default font size for views -- cannot do this
    // in Preferences, which is in base and not supposed to use QtGui
    int viewFontSize = int(QApplication::font().pointSize() * 0.9);
    QSettings settings;
    settings.beginGroup("Preferences");
    viewFontSize = settings.value("view-font-size", viewFontSize).toInt();
    settings.setValue("view-font-size", viewFontSize);
    settings.endGroup();

#ifdef NOT_DEFINED // This no longer works correctly on any platform AFAICS
    Preferences::BackgroundMode mode =
        Preferences::getInstance()->getBackgroundMode();
    m_initialDarkBackground = m_viewManager->getGlobalDarkBackground();
    if (mode != Preferences::BackgroundFromTheme) {
        m_viewManager->setGlobalDarkBackground
            (mode == Preferences::DarkBackground);
    }
#endif

    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)));
    connect(m_paneStack, SIGNAL(paneDeleteButtonClicked(Pane *)),
            this, SLOT(paneDeleteButtonClicked(Pane *)));

    m_playSource = new AudioCallbackPlaySource(m_viewManager,
                                               QApplication::applicationName());
    if (m_soundOptions & WithAudioInput) {
        m_recordTarget = new AudioRecordTarget(m_viewManager,
                                               QApplication::applicationName());
        connect(m_recordTarget, SIGNAL(recordDurationChanged(sv_frame_t, sv_samplerate_t)),
                this, SLOT(recordDurationChanged(sv_frame_t, sv_samplerate_t)));
    }

    connect(m_playSource, SIGNAL(sampleRateMismatch(sv_samplerate_t, sv_samplerate_t, bool)),
	    this,           SLOT(sampleRateMismatch(sv_samplerate_t, sv_samplerate_t, bool)));
    connect(m_playSource, SIGNAL(channelCountIncreased(int)),
            this,           SLOT(audioChannelCountIncreased(int)));
    connect(m_playSource, SIGNAL(audioOverloadPluginDisabled()),
            this,           SLOT(audioOverloadPluginDisabled()));
    connect(m_playSource, SIGNAL(audioTimeStretchMultiChannelDisabled()),
            this,           SLOT(audioTimeStretchMultiChannelDisabled()));

    connect(m_viewManager, SIGNAL(outputLevelsChanged(float, float)),
	    this, SLOT(outputLevelsChanged(float, float)));

    connect(m_viewManager, SIGNAL(playbackFrameChanged(sv_frame_t)),
            this, SLOT(playbackFrameChanged(sv_frame_t)));

    connect(m_viewManager, SIGNAL(globalCentreFrameChanged(sv_frame_t)),
            this, SLOT(globalCentreFrameChanged(sv_frame_t)));

    connect(m_viewManager, SIGNAL(viewCentreFrameChanged(View *, sv_frame_t)),
            this, SLOT(viewCentreFrameChanged(View *, sv_frame_t)));

    connect(m_viewManager, SIGNAL(viewZoomLevelChanged(View *, int, bool)),
            this, SLOT(viewZoomLevelChanged(View *, int, bool)));

    connect(Preferences::getInstance(),
            SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
            this,
            SLOT(preferenceChanged(PropertyContainer::PropertyName)));

    Labeller::ValueType labellerType = Labeller::ValueFromTwoLevelCounter;
    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);

    if (m_soundOptions & WithMIDIInput) {
        m_midiInput = new MIDIInput(QApplication::applicationName(), this);
    }

    QTimer::singleShot(1500, this, SIGNAL(hideSplash()));
}

MainWindowBase::~MainWindowBase()
{
    SVDEBUG << "MainWindowBase::~MainWindowBase" << endl;

    // We have to delete the breakfastquay::SystemPlaybackTarget or
    // breakfastquay::SystemAudioIO object (whichever we have -- it
    // depends on whether we handle recording or not) before we delete
    // the ApplicationPlaybackSource and ApplicationRecordTarget that
    // they refer to.

    deleteAudioIO();

    // Then delete the Application objects.
    delete m_playSource;
    delete m_recordTarget;
    
    delete m_viewManager;
    delete m_oscQueue;
    delete m_oscQueueStarter;
    delete m_midiInput;
    Profiles::getInstance()->dump();
}

void
MainWindowBase::emitHideSplash()
{
    emit hideSplash(this);
}

void
MainWindowBase::finaliseMenus()
{
    delete m_menuShortcutMapper;
    m_menuShortcutMapper = 0;

    foreach (QShortcut *sc, m_appShortcuts) {
        delete sc;
    }
    m_appShortcuts.clear();

    QMenuBar *mb = menuBar();

    // This used to find all children of QMenu type, and call
    // finaliseMenu on those. But it seems we are getting hold of some
    // menus that way that are not actually active in the menu bar and
    // are not returned in their parent menu's actions() list, and if
    // we finalise those, we end up with duplicate shortcuts in the
    // app shortcut mapper. So we should do this by descending the
    // menu tree through only those menus accessible via actions()
    // from their parents instead.

    QList<QMenu *> menus = mb->findChildren<QMenu *>
        (QString(), Qt::FindDirectChildrenOnly);

    foreach (QMenu *menu, menus) {
        if (menu) finaliseMenu(menu);
    }
}

void
MainWindowBase::finaliseMenu(QMenu *menu)
{
    foreach (QAction *a, menu->actions()) {
        a->setIconVisibleInMenu(m_iconsVisibleInMenus);
    }

#ifdef Q_OS_MAC
    // See https://bugreports.qt-project.org/browse/QTBUG-38256 and
    // our issue #890 http://code.soundsoftware.ac.uk/issues/890 --
    // single-key shortcuts that are associated only with a menu
    // action (and not with a toolbar button) do not work with Qt 5.x
    // under OS/X.
    // 
    // Apparently Cocoa never handled them as a matter of course, but
    // earlier versions of Qt picked them up as widget shortcuts and
    // handled them anyway. That behaviour was removed to fix a crash
    // when invoking a menu while its window was overridden by a modal
    // dialog (https://bugreports.qt-project.org/browse/QTBUG-30657).
    //
    // This workaround restores the single-key shortcut behaviour by
    // searching in menus for single-key shortcuts that are associated
    // only with the menu and not with a toolbar button, and
    // augmenting them with global application shortcuts that invoke
    // the relevant actions, testing whether the actions are enabled
    // on invocation.
    //
    // (Previously this acted on all single-key shortcuts in menus,
    // and it removed the shortcut from the action when it created
    // each new global one, in order to avoid an "ambiguous shortcut"
    // error in the case where the action was also associated with a
    // toolbar button. But that has the unwelcome side-effect of
    // removing the shortcut hint from the menu entry. So now we leave
    // the shortcut in the menu action as well as creating a global
    // one, and we only act on shortcuts that have no toolbar button,
    // i.e. that will not otherwise work. The downside is that if this
    // bug is fixed in a future Qt release, we will start getting
    // "ambiguous shortcut" errors from the menu entry actions and
    // will need to update the code.)

    // Update: The bug was fixed in Qt 5.4 for shortcuts with no
    // modifier, and I believe it is fixed in Qt 5.5 for shortcuts
    // with Shift modifiers. The below reflects that

#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0))

    if (!m_menuShortcutMapper) {
        m_menuShortcutMapper = new QSignalMapper(this);
        connect(m_menuShortcutMapper, SIGNAL(mapped(QObject *)),
                this, SLOT(menuActionMapperInvoked(QObject *)));
    }

    foreach (QAction *a, menu->actions()) {

        if (a->isSeparator()) {
            continue;
        } else if (a->menu()) {
            finaliseMenu(a->menu());
        } else {

            QWidgetList ww = a->associatedWidgets();
            bool hasButton = false;
            foreach (QWidget *w, ww) {
                if (qobject_cast<QAbstractButton *>(w)) {
                    hasButton = true;
                    break;
                }
            }
            if (hasButton) continue;
            QKeySequence sc = a->shortcut();

            // Note that the set of "single-key shortcuts" that aren't
            // working and that we need to handle here includes those
            // with the Shift modifier mask as well as those with no
            // modifier at all
#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
            // Nothing needed
            if (false) {
#elif (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
            if (sc.count() == 1 &&
                (sc[0] & Qt::KeyboardModifierMask) == Qt::ShiftModifier) {
#else
            if (sc.count() == 1 &&
                ((sc[0] & Qt::KeyboardModifierMask) == Qt::NoModifier ||
                 (sc[0] & Qt::KeyboardModifierMask) == Qt::ShiftModifier)) {
#endif
                QShortcut *newSc = new QShortcut(sc, a->parentWidget());
                QObject::connect(newSc, SIGNAL(activated()),
                                 m_menuShortcutMapper, SLOT(map()));
                m_menuShortcutMapper->setMapping(newSc, a);
                m_appShortcuts.push_back(newSc);
            }
        }
    }
#endif
#endif
}

void
MainWindowBase::menuActionMapperInvoked(QObject *o)
{
    QAction *a = qobject_cast<QAction *>(o);
    if (a && a->isEnabled()) {
        a->trigger();
    }
}

void
MainWindowBase::resizeConstrained(QSize size)
{
    QDesktopWidget *desktop = QApplication::desktop();
    QRect available = desktop->availableGeometry();
    QSize actual(std::min(size.width(), available.width()),
                 std::min(size.height(), available.height()));
    resize(actual);
}

void
MainWindowBase::startOSCQueue()
{
    m_oscQueueStarter = new OSCQueueStarter(this);
    connect(m_oscQueueStarter, SIGNAL(finished()), this, SLOT(oscReady()));
    m_oscQueueStarter->start();
}

void
MainWindowBase::oscReady()
{
    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);
        cerr << "Finished setting up OSC interface" << endl;
    }
}

QString
MainWindowBase::getOpenFileName(FileFinder::FileType type)
{
    FileFinder *ff = FileFinder::getInstance();

    if (type == 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);
        }
    }        

    QString lastPath = m_sessionFile;

    if (type == FileFinder::AudioFile) {
        lastPath = m_audioFile;
    }

    return ff->getOpenFileName(type, lastPath);
}

QString
MainWindowBase::getSaveFileName(FileFinder::FileType type)
{
    QString lastPath = m_sessionFile;

    if (type == FileFinder::AudioFile) {
        lastPath = m_audioFile;
    }

    FileFinder *ff = FileFinder::getInstance();
    return ff->getSaveFileName(type, lastPath);
}

void
MainWindowBase::registerLastOpenedFilePath(FileFinder::FileType type, QString path)
{
    FileFinder *ff = FileFinder::getInstance();
    ff->registerLastOpenedFilePath(type, path);
}

QString
MainWindowBase::getDefaultSessionTemplate() const
{
    QSettings settings;
    settings.beginGroup("MainWindow");
    QString templateName = settings.value("sessiontemplate", "").toString();
    if (templateName == "") templateName = "default";
    return templateName;
}

void
MainWindowBase::setDefaultSessionTemplate(QString n) 
{
    QSettings settings;
    settings.beginGroup("MainWindow");
    settings.setValue("sessiontemplate", n);
}    

void
MainWindowBase::updateMenuStates()
{
    Pane *currentPane = 0;
    Layer *currentLayer = 0;

    if (m_paneStack) currentPane = m_paneStack->getCurrentPane();
    if (currentPane) currentLayer = currentPane->getSelectedLayer();

    bool havePrevPane = false, haveNextPane = false;
    bool havePrevLayer = false, haveNextLayer = false;

    if (currentPane) {
        for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
            if (m_paneStack->getPane(i) == currentPane) {
                if (i > 0) havePrevPane = true;
                if (i < m_paneStack->getPaneCount()-1) haveNextPane = true;
                break;
            }
        }
        // the prev/next layer commands actually include the pane
        // itself as one of the selectables -- so we always have a
        // prev and next layer, as long as we have a pane with at
        // least one layer in it
        if (currentPane->getLayerCount() > 0) {
            havePrevLayer = true;
            haveNextLayer = true;
        }
    }        

    bool haveCurrentPane =
        (currentPane != 0);
    bool haveCurrentLayer =
        (haveCurrentPane &&
         (currentLayer != 0));
    bool haveMainModel =
	(getMainModel() != 0);
    bool havePlayTarget =
	(m_playTarget != 0 || m_audioIO != 0);
    bool haveSelection = 
	(m_viewManager &&
	 !m_viewManager->getSelections().empty());
    bool haveCurrentEditableLayer =
	(haveCurrentLayer &&
	 currentLayer->isLayerEditable());
    bool haveCurrentTimeInstantsLayer = 
	(haveCurrentLayer &&
	 dynamic_cast<TimeInstantLayer *>(currentLayer));
    bool haveCurrentDurationLayer = 
	(haveCurrentLayer &&
	 (dynamic_cast<NoteLayer *>(currentLayer) ||
	  dynamic_cast<FlexiNoteLayer *>(currentLayer) ||
          dynamic_cast<RegionLayer *>(currentLayer)));
    bool haveCurrentColour3DPlot =
        (haveCurrentLayer &&
         dynamic_cast<Colour3DPlotLayer *>(currentLayer));
    bool haveClipboardContents =
        (m_viewManager &&
         !m_viewManager->getClipboard().empty());
    bool haveTabularLayer =
        (haveCurrentLayer &&
         dynamic_cast<TabularModel *>(currentLayer->getModel()));

    emit canAddPane(haveMainModel);
    emit canDeleteCurrentPane(haveCurrentPane);
    emit canZoom(haveMainModel && haveCurrentPane);
    emit canScroll(haveMainModel && haveCurrentPane);
    emit canAddLayer(haveMainModel && haveCurrentPane);
    emit canImportMoreAudio(haveMainModel);
    emit canReplaceMainAudio(haveMainModel);
    emit canImportLayer(haveMainModel && haveCurrentPane);
    emit canExportAudio(haveMainModel);
    emit canChangeSessionTemplate(haveMainModel);
    emit canExportLayer(haveMainModel &&
                        (haveCurrentEditableLayer || haveCurrentColour3DPlot));
    emit canExportImage(haveMainModel && haveCurrentPane);
    emit canDeleteCurrentLayer(haveCurrentLayer);
    emit canRenameLayer(haveCurrentLayer);
    emit canEditLayer(haveCurrentEditableLayer);
    emit canEditLayerTabular(haveCurrentEditableLayer || haveTabularLayer);
    emit canMeasureLayer(haveCurrentLayer);
    emit canSelect(haveMainModel && haveCurrentPane);
    emit canPlay(haveMainModel && havePlayTarget);
    emit canRecord(m_recordTarget != 0);
    emit canFfwd(haveMainModel);
    emit canRewind(haveMainModel);
    emit canPaste(haveClipboardContents);
    emit canInsertInstant(haveCurrentPane);
    emit canInsertInstantsAtBoundaries(haveCurrentPane && haveSelection);
    emit canInsertItemAtSelection(haveCurrentPane && haveSelection && haveCurrentDurationLayer);
    emit canRenumberInstants(haveCurrentTimeInstantsLayer && haveSelection);
    emit canSubdivideInstants(haveCurrentTimeInstantsLayer && haveSelection);
    emit canWinnowInstants(haveCurrentTimeInstantsLayer && haveSelection);
    emit canPlaySelection(haveMainModel && havePlayTarget && haveSelection);
    emit canClearSelection(haveSelection);
    emit canEditSelection(haveSelection && haveCurrentEditableLayer);
    emit canSave(m_sessionFile != "" && m_documentModified);
    emit canSaveAs(haveMainModel);
    emit canSelectPreviousPane(havePrevPane);
    emit canSelectNextPane(haveNextPane);
    emit canSelectPreviousLayer(havePrevLayer);
    emit canSelectNextLayer(haveNextLayer);
}

void
MainWindowBase::documentModified()
{
//    SVDEBUG << "MainWindowBase::documentModified" << endl;

    if (!m_documentModified) {
        //!!! this in subclass implementation?
	setWindowTitle(tr("%1 (modified)").arg(windowTitle()));
    }

    m_documentModified = true;
    updateMenuStates();
}

void
MainWindowBase::documentRestored()
{
//    SVDEBUG << "MainWindowBase::documentRestored" << 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();

    // What we want here is not the currently playing frame (unless we
    // are about to clear out the audio playback buffers -- which may
    // or may not be possible, depending on the audio driver).  What
    // we want is the frame that was last committed to the soundcard
    // buffers, as the audio driver will continue playing up to that
    // frame before switching to whichever one we decide we want to
    // switch to, regardless of our efforts.

    sv_frame_t frame = m_playSource->getCurrentBufferedFrame();

    cerr << "currentPaneChanged: current frame (in ref model) = " << frame << endl;

    View::ModelSet soloModels = p->getModels();
    
    View::ModelSet sources;
    for (View::ModelSet::iterator mi = soloModels.begin();
         mi != soloModels.end(); ++mi) {
        // If a model in this pane is derived from something else,
        // then we want to play that model as well -- if the model
        // that's derived from it is not something that is itself
        // individually playable (e.g. a waveform)
        if (*mi &&
            !dynamic_cast<RangeSummarisableTimeValueModel *>(*mi) &&
            (*mi)->getSourceModel()) {
            sources.insert((*mi)->getSourceModel());
        }
    }
    for (View::ModelSet::iterator mi = sources.begin();
         mi != sources.end(); ++mi) {
        soloModels.insert(*mi);
    }

    //!!! Need an "atomic" way of telling the play source that the
    //playback model has changed, and changing it on ViewManager --
    //the play source should be making the setPlaybackModel call to
    //ViewManager

    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)) {
        if (m_playSource->isPlaying()) m_playSource->play(frame);
    }
}

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;

    sv_frame_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(currentPane, *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(currentPane, *i, clipboard);
    }
}

void
MainWindowBase::paste()
{
    pasteRelative(0);
}

void
MainWindowBase::pasteAtPlaybackPosition()
{
    sv_frame_t pos = getFrame();
    Clipboard &clipboard = m_viewManager->getClipboard();
    if (!clipboard.empty()) {
        sv_frame_t firstEventFrame = clipboard.getPoints()[0].getFrame();
        sv_frame_t offset = 0;
        if (firstEventFrame < 0) {
            offset = pos - firstEventFrame;
        } else if (firstEventFrame < pos) {
            offset = pos - firstEventFrame;
        } else {
            offset = -(firstEventFrame - pos);
        }
        pasteRelative(offset);
    }
}

void
MainWindowBase::pasteRelative(sv_frame_t offset)
{
    Pane *currentPane = m_paneStack->getCurrentPane();
    if (!currentPane) return;

    Layer *layer = currentPane->getSelectedLayer();

    Clipboard &clipboard = m_viewManager->getClipboard();

    bool inCompound = false;

    if (!layer || !layer->isLayerEditable()) {
        
        CommandHistory::getInstance()->startCompoundOperation
            (tr("Paste"), true);

        // no suitable current layer: create one of the most
        // appropriate sort
        LayerFactory::LayerType type =
            LayerFactory::getInstance()->getLayerTypeForClipboardContents(clipboard);
        layer = m_document->createEmptyLayer(type);

        if (!layer) {
            CommandHistory::getInstance()->endCompoundOperation();
            return;
        }

        m_document->addLayerToView(currentPane, layer);
        m_paneStack->setCurrentLayer(currentPane, layer);

        inCompound = true;
    }

    layer->paste(currentPane, clipboard, offset, true);

    if (inCompound) CommandHistory::getInstance()->endCompoundOperation();
}

void
MainWindowBase::deleteSelected()
{
    if (m_paneStack->getCurrentPane() &&
	m_paneStack->getCurrentPane()->getSelectedLayer()) {
        
        Layer *layer = m_paneStack->getCurrentPane()->getSelectedLayer();

        if (m_viewManager) {

            if (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);
                }
            }
	}
    }
}

// FrameTimer method

sv_frame_t
MainWindowBase::getFrame() const
{
    if (m_playSource && m_playSource->isPlaying()) {
        return m_playSource->getCurrentPlayingFrame();
    } else {
        return m_viewManager->getPlaybackFrame();
    }
}    

void
MainWindowBase::insertInstant()
{
    insertInstantAt(getFrame());
}

void
MainWindowBase::insertInstantsAtBoundaries()
{
    MultiSelection::SelectionList selections = m_viewManager->getSelections();
    for (MultiSelection::SelectionList::iterator i = selections.begin();
         i != selections.end(); ++i) {
        sv_frame_t start = i->getStartFrame();
        sv_frame_t end = i->getEndFrame();
        if (start != end) {
            insertInstantAt(start);
            insertInstantAt(end);
        }
    }
}

void
MainWindowBase::insertInstantAt(sv_frame_t frame)
{
    Pane *pane = m_paneStack->getCurrentPane();
    if (!pane) {
        return;
    }

    frame = pane->alignFromReference(frame);

    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) {

                if (m_labeller->requiresPrevPoint()) {

                    SparseOneDimensionalModel::PointList prevPoints =
                        sodm->getPreviousPoints(frame);

                    if (!prevPoints.empty()) {
                        prevPoint = *prevPoints.begin();
                        havePrevPoint = true;
                    }
                }

                m_labeller->setSampleRate(sodm->getSampleRate());

                if (m_labeller->actingOnPrevPoint() && havePrevPoint) {
                    command->deletePoint(prevPoint);
                }

                m_labeller->label<SparseOneDimensionalModel::Point>
                    (point, havePrevPoint ? &prevPoint : 0);

                if (m_labeller->actingOnPrevPoint() && 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 *c = command->finish();
            if (c) CommandHistory::getInstance()->addCommand(c, false);
        }
    }
}

void
MainWindowBase::insertItemAtSelection()
{
    MultiSelection::SelectionList selections = m_viewManager->getSelections();
    for (MultiSelection::SelectionList::iterator i = selections.begin();
         i != selections.end(); ++i) {
        sv_frame_t start = i->getStartFrame();
        sv_frame_t end = i->getEndFrame();
        if (start < end) {
            insertItemAt(start, end - start);
        }
    }
}

void
MainWindowBase::insertItemAt(sv_frame_t frame, sv_frame_t duration)
{
    Pane *pane = m_paneStack->getCurrentPane();
    if (!pane) {
        return;
    }

    // ugh!

    sv_frame_t alignedStart = pane->alignFromReference(frame);
    sv_frame_t alignedEnd = pane->alignFromReference(frame + duration);
    if (alignedStart >= alignedEnd) return;
    sv_frame_t alignedDuration = alignedEnd - alignedStart;

    Command *c = 0;

    QString name = tr("Add Item at %1 s")
        .arg(RealTime::frame2RealTime
             (alignedStart,
              getMainModel()->getSampleRate())
             .toText(false).c_str());

    Layer *layer = pane->getSelectedLayer();
    if (!layer) return;

    RegionModel *rm = dynamic_cast<RegionModel *>(layer->getModel());
    if (rm) {
        RegionModel::Point point(alignedStart,
                                 rm->getValueMaximum() + 1,
                                 alignedDuration,
                                 "");
        RegionModel::EditCommand *command =
            new RegionModel::EditCommand(rm, tr("Add Point"));
        command->addPoint(point);
        command->setName(name);
        c = command->finish();
    }

    if (c) {
        CommandHistory::getInstance()->addCommand(c, false);
        return;
    }

    NoteModel *nm = dynamic_cast<NoteModel *>(layer->getModel());
    if (nm) {
        NoteModel::Point point(alignedStart,
                               nm->getValueMinimum(),
                               alignedDuration,
                               1.f,
                               "");
        NoteModel::EditCommand *command =
            new NoteModel::EditCommand(nm, tr("Add Point"));
        command->addPoint(point);
        command->setName(name);
        c = command->finish();
    }

    if (c) {
        CommandHistory::getInstance()->addCommand(c, false);
        return;
    }

    FlexiNoteModel *fnm = dynamic_cast<FlexiNoteModel *>(layer->getModel());
    if (fnm) {
        FlexiNoteModel::Point point(alignedStart,
                                    fnm->getValueMinimum(),
                                    alignedDuration,
                                    1.f,
                                    "");
        FlexiNoteModel::EditCommand *command =
            new FlexiNoteModel::EditCommand(fnm, tr("Add Point"));
        command->addPoint(point);
        command->setName(name);
        c = command->finish();
    }
    
    if (c) {
        CommandHistory::getInstance()->addCommand(c, false);
        return;
    }
}

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());

    Command *c = labeller.labelAll<SparseOneDimensionalModel::Point>(*sodm, &ms);
    if (c) CommandHistory::getInstance()->addCommand(c, false);
}

void
MainWindowBase::subdivideInstantsBy(int n)
{
    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());

    Command *c = labeller.subdivide<SparseOneDimensionalModel::Point>
        (*sodm, &ms, n);
    if (c) CommandHistory::getInstance()->addCommand(c, false);
}

void
MainWindowBase::winnowInstantsBy(int n)
{
    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());

    Command *c = labeller.winnow<SparseOneDimensionalModel::Point>
        (*sodm, &ms, n);
    if (c) CommandHistory::getInstance()->addCommand(c, false);
}

MainWindowBase::FileOpenStatus
MainWindowBase::openPath(QString fileOrUrl, AudioFileOpenMode mode)
{
    ProgressDialog dialog(tr("Opening file or URL..."), true, 2000, this);
    connect(&dialog, SIGNAL(showing()), this, SIGNAL(hideSplash()));
    return open(FileSource(fileOrUrl, &dialog), 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);

    bool rdf = (source.getExtension().toLower() == "rdf" ||
                source.getExtension().toLower() == "n3" ||
                source.getExtension().toLower() == "ttl");

    bool audio = AudioFileReaderFactory::getKnownExtensions().contains
        (source.getExtension().toLower());

    bool rdfSession = false;
    if (rdf) {
        RDFImporter::RDFDocumentType rdfType = 
            RDFImporter::identifyDocumentType
            (QUrl::fromLocalFile(source.getLocalFilename()).toString());
        if (rdfType == RDFImporter::AudioRefAndAnnotations ||
            rdfType == RDFImporter::AudioRef) {
            rdfSession = true;
        } else if (rdfType == RDFImporter::NotRDF) {
            rdf = false;
        }
    }

    if (rdf) {
        if (rdfSession) {
            bool cancel = false;
            if (!canImportLayer || shouldCreateNewSessionForRDFAudio(&cancel)) {
                return openSession(source);
            } else if (cancel) {
                return FileOpenCancelled;
            } else {
                return openLayer(source);
            }
        } else {
            if ((status = openSession(source)) != FileOpenFailed) {
                return status;
            } else if (!canImportLayer) {
                return FileOpenWrongMode;
            } else if ((status = openLayer(source)) != FileOpenFailed) {
                return status;
            } else {
                return FileOpenFailed;
            }
        }
    }

    if (audio && (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,
                          QString templateName)
{
    SVDEBUG << "MainWindowBase::openAudio(" << source.getLocation() << ") with mode " << mode << " and template " << templateName << endl;

    if (templateName == "") {
        templateName = getDefaultSessionTemplate();
    }

//    cerr << "template is: \"" << templateName << "\"" << endl;

    if (!source.isAvailable()) {
        if (source.wasCancelled()) {
            return FileOpenCancelled;
        } else {
            return FileOpenFailed;
        }
    }

    source.waitForData();

    m_openingAudioFile = true;

    sv_samplerate_t rate = 0;

    if (Preferences::getInstance()->getFixedSampleRate() != 0) {
        rate = Preferences::getInstance()->getFixedSampleRate();
    } else if (Preferences::getInstance()->getResampleOnLoad()) {
        if (getMainModel()) {
            rate = getMainModel()->getSampleRate();
        }
    }

    ReadOnlyWaveFileModel *newModel = new ReadOnlyWaveFileModel(source, rate);

    if (!newModel->isOK()) {
	delete newModel;
        m_openingAudioFile = false;
        if (source.wasCancelled()) {
            return FileOpenCancelled;
        } else { 
            return FileOpenFailed;
        }
    }

//    cerr << "mode = " << mode << endl;

    if (mode == AskUser) {
        if (getMainModel()) {

            QSettings settings;
            settings.beginGroup("MainWindow");
            int lastMode = settings.value("lastaudioopenmode", 0).toBool();
            settings.endGroup();
            int imode = 0;
            
            QStringList items;
            items << tr("Close the current session and start a new one")
                  << tr("Replace the main audio file in this session")
                  << tr("Add the audio file to this session");

            bool ok = false;
            QString item = ListInputDialog::getItem
                (this, tr("Select target for import"),
                 tr("<b>Select a target for import</b><p>You already have an audio file loaded.<br>What would you like to do with the new audio file?"),
                 items, lastMode, &ok);
            
            if (!ok || item.isEmpty()) {
                delete newModel;
                m_openingAudioFile = false;
                return FileOpenCancelled;
            }
            
            for (int i = 0; i < items.size(); ++i) {
                if (item == items[i]) imode = i;
            }

            settings.beginGroup("MainWindow");
            settings.setValue("lastaudioopenmode", imode);
            settings.endGroup();

            mode = (AudioFileOpenMode)imode;

        } else {
            // no main model: make a new session
            mode = ReplaceSession;
        }
    }

    if (mode == ReplaceCurrentPane) {

        Pane *pane = m_paneStack->getCurrentPane();
        if (pane) {
            if (getMainModel()) {
                View::ModelSet models(pane->getModels());
                if (models.find(getMainModel()) != models.end()) {
                    // Current pane contains main model: replace that
                    mode = ReplaceMainModel;
                }
                // Otherwise the current pane has a non-default model,
                // which we will deal with later
            } else {
                // We have no main model, so start a new session with
                // optional template
                mode = ReplaceSession;
            }
        } else {
            // We seem to have no current pane!  Oh well
            mode = CreateAdditionalModel;
        }
    }

    if (mode == CreateAdditionalModel && !getMainModel()) {
        SVDEBUG << "Mode is CreateAdditionalModel but we have no main model, switching to ReplaceSession mode" << endl;
        mode = ReplaceSession;
    }

    bool loadedTemplate = false;

    if (mode == ReplaceSession) {

        if (!checkSaveModified()) return FileOpenCancelled;

        SVDEBUG << "SV looking for template " << templateName << endl;
        if (templateName != "") {
            FileOpenStatus tplStatus = openSessionTemplate(templateName);
            if (tplStatus == FileOpenCancelled) {
                cerr << "Template load cancelled" << endl;
                return FileOpenCancelled;
            }
            if (tplStatus != FileOpenFailed) {
                cerr << "Template load succeeded" << endl;
                loadedTemplate = true;
            }
        }

        if (!loadedTemplate) {
            SVDEBUG << "No template found: closing session, creating new empty document" << endl;
            closeSession();
            createDocument();
        }

        SVDEBUG << "Now switching to ReplaceMainModel mode" << endl;
        mode = ReplaceMainModel;
    }

    emit activity(tr("Import audio file \"%1\"").arg(source.getLocation()));

    if (mode == ReplaceMainModel) {

        Model *prevMain = getMainModel();
        if (prevMain) {
            m_playSource->removeModel(prevMain);
            PlayParameterRepository::getInstance()->removePlayable(prevMain);
        }
        PlayParameterRepository::getInstance()->addPlayable(newModel);

        SVDEBUG << "SV about to call setMainModel(" << newModel << "): prevMain is " << prevMain << endl;

	m_document->setMainModel(newModel);

	setupMenus();

	if (loadedTemplate || (m_sessionFile == "")) {
            //!!! shouldn't be dealing directly with title from here -- call a method
	    setWindowTitle(tr("%1: %2")
                           .arg(QApplication::applicationName())
                           .arg(source.getLocation()));
	    CommandHistory::getInstance()->clear();
	    CommandHistory::getInstance()->documentSaved();
	    m_documentModified = false;
	} else {
	    setWindowTitle(tr("%1: %2 [%3]")
                           .arg(QApplication::applicationName())
			   .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.getBasename()), true);

	m_document->addImportedModel(newModel);

	AddPaneCommand *command = new AddPaneCommand(this);
	CommandHistory::getInstance()->addCommand(command);

	Pane *pane = command->getPane();

        if (m_timeRulerLayer) {
            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.getBasename()), 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());

    emit audioFileLoaded();

    return FileOpenSucceeded;
}

MainWindowBase::FileOpenStatus
MainWindowBase::openPlaylist(FileSource source, AudioFileOpenMode mode)
{
    SVDEBUG << "MainWindowBase::openPlaylist(" << source.getLocation() << ")" << endl;

    std::set<QString> extensions;
    PlaylistFileReader::getSupportedExtensions(extensions);
    QString extension = source.getExtension().toLower();
    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) {

        ProgressDialog dialog(tr("Opening playlist..."), true, 2000, this);
        connect(&dialog, SIGNAL(showing()), this, SIGNAL(hideSplash()));
        FileOpenStatus status = openAudio(FileSource(*i, &dialog), 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)
{
    SVDEBUG << "MainWindowBase::openLayer(" << source.getLocation() << ")" << endl;

    Pane *pane = m_paneStack->getCurrentPane();
    
    if (!pane) {
	// shouldn't happen, as the menu action should have been disabled
	cerr << "WARNING: MainWindowBase::openLayer: no current pane" << endl;
	return FileOpenWrongMode;
    }

    if (!getMainModel()) {
	// shouldn't happen, as the menu action should have been disabled
	cerr << "WARNING: MainWindowBase::openLayer: No main model -- hence no default sample rate available" << endl;
	return FileOpenWrongMode;
    }

    if (!source.isAvailable()) return FileOpenFailed;
    source.waitForData();

    QString path = source.getLocalFilename();

    RDFImporter::RDFDocumentType rdfType = 
        RDFImporter::identifyDocumentType(QUrl::fromLocalFile(path).toString());

//    cerr << "RDF type:  (in layer) " << (int) rdfType << endl;

    if (rdfType != RDFImporter::NotRDF) {

        return openLayersFromRDF(source);

    } else if (source.getExtension().toLower() == "svl" ||
               (source.getExtension().toLower() == "xml" &&
                (SVFileReader::identifyXmlFile(source.getLocalFilename())
                 == SVFileReader::SVLayerFile))) {

        PaneCallback callback(this);
        QFile file(path);
        
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            cerr << "ERROR: MainWindowBase::openLayer("
                      << source.getLocation()
                      << "): Failed to open file for reading" << endl;
            return FileOpenFailed;
        }
        
        SVFileReader reader(m_document, callback, source.getLocation());
        connect
            (&reader, SIGNAL(modelRegenerationFailed(QString, QString, QString)),
             this, SLOT(modelRegenerationFailed(QString, QString, QString)));
        connect
            (&reader, SIGNAL(modelRegenerationWarning(QString, QString, QString)),
             this, SLOT(modelRegenerationWarning(QString, QString, QString)));
        reader.setCurrentPane(pane);
        
        QXmlInputSource inputSource(&file);
        reader.parse(inputSource);
        
        if (!reader.isOK()) {
            cerr << "ERROR: MainWindowBase::openLayer("
                      << source.getLocation()
                      << "): Failed to read XML file: "
                      << reader.getErrorString() << endl;
            return FileOpenFailed;
        }

        emit activity(tr("Import layer XML file \"%1\"").arg(source.getLocation()));

        m_recentFiles.addFile(source.getLocation());

        if (!source.isRemote()) {
            registerLastOpenedFilePath(FileFinder::LayerFile, path); // for file dialog
        }

        return FileOpenSucceeded;

    } else {
        
        try {

            MIDIFileImportDialog midiDlg(this);

            Model *model = DataFileReaderFactory::loadNonCSV
                (path, &midiDlg, getMainModel()->getSampleRate());
        
            if (!model) {
                CSVFormat format(path);
                format.setSampleRate(getMainModel()->getSampleRate());
                CSVFormatDialog *dialog = new CSVFormatDialog(this, format);
                if (dialog->exec() == QDialog::Accepted) {
                    model = DataFileReaderFactory::loadCSV
                        (path, dialog->getFormat(),
                         getMainModel()->getSampleRate());
                }
            }

            if (model) {

                SVDEBUG << "MainWindowBase::openLayer: Have model" << endl;

                emit activity(tr("Import MIDI file \"%1\"").arg(source.getLocation()));

                Layer *newLayer = m_document->createImportedLayer(model);

                if (newLayer) {

                    m_document->addLayerToView(pane, newLayer);
                    m_paneStack->setCurrentLayer(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;
            }
        }
    }
    
    return FileOpenFailed;
}

MainWindowBase::FileOpenStatus
MainWindowBase::openImage(FileSource source)
{
    SVDEBUG << "MainWindowBase::openImage(" << source.getLocation() << ")" << endl;

    Pane *pane = m_paneStack->getCurrentPane();
    
    if (!pane) {
	// shouldn't happen, as the menu action should have been disabled
	cerr << "WARNING: MainWindowBase::openImage: no current pane" << 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

    cerr << "openImage: trying location \"" << source.getLocation() << "\" in image layer" << endl;

    if (!il->addImage(m_viewManager->getGlobalCentreFrame(), source.getLocation())) {
        if (newLayer) {
            m_document->deleteLayer(il); // also releases its model
        }
        return FileOpenFailed;
    } else {
        if (newLayer) {
            m_document->addLayerToView(pane, il);
        }
        m_paneStack->setCurrentLayer(pane, il);
    }

    return FileOpenSucceeded;
}

MainWindowBase::FileOpenStatus
MainWindowBase::openDirOfAudio(QString dirPath)
{
    QDir dir(dirPath);
    QStringList files = dir.entryList(QDir::Files | QDir::Readable);
    files.sort();

    FileOpenStatus status = FileOpenFailed;
    bool first = true;
    bool cancelled = false;

    foreach (QString file, files) {

        FileSource source(dir.filePath(file));
        if (!source.isAvailable()) {
            continue;
        }

        if (AudioFileReaderFactory::getKnownExtensions().contains
            (source.getExtension().toLower())) {
            
            AudioFileOpenMode mode = CreateAdditionalModel;
            if (first) mode = ReplaceSession;
            
            switch (openAudio(source, mode)) {
            case FileOpenSucceeded:
                status = FileOpenSucceeded;
                first = false;
                break;
            case FileOpenFailed:
                break;
            case FileOpenCancelled:
                cancelled = true;
                break;
            case FileOpenWrongMode:
                break;
            }
        }

        if (cancelled) break;
    }

    return status;
}

MainWindowBase::FileOpenStatus
MainWindowBase::openSessionPath(QString fileOrUrl)
{
    ProgressDialog dialog(tr("Opening session..."), true, 2000, this);
    connect(&dialog, SIGNAL(showing()), this, SIGNAL(hideSplash()));
    return openSession(FileSource(fileOrUrl, &dialog));
}

MainWindowBase::FileOpenStatus
MainWindowBase::openSession(FileSource source)
{
    SVDEBUG << "MainWindowBase::openSession(" << source.getLocation() << ")" << endl;

    if (!source.isAvailable()) return FileOpenFailed;
    source.waitForData();

    QString sessionExt = 
        InteractiveFileFinder::getInstance()->getApplicationSessionExtension();

    if (source.getExtension().toLower() != sessionExt) {

        RDFImporter::RDFDocumentType rdfType = 
            RDFImporter::identifyDocumentType
            (QUrl::fromLocalFile(source.getLocalFilename()).toString());

//        cerr << "RDF type: " << (int)rdfType << endl;

        if (rdfType == RDFImporter::AudioRefAndAnnotations ||
            rdfType == RDFImporter::AudioRef) {
            return openSessionFromRDF(source);
        } else if (rdfType != RDFImporter::NotRDF) {
            return FileOpenFailed;
        }

        if (source.getExtension().toLower() == "xml") {
            if (SVFileReader::identifyXmlFile(source.getLocalFilename()) ==
                SVFileReader::SVSessionFile) {
                cerr << "This XML file looks like a session file, attempting to open it as a session" << endl;
            } else {
                return FileOpenFailed;
            }
        } else {
            return FileOpenFailed;
        }
    }

    QXmlInputSource *inputSource = 0;
    BZipFileDevice *bzFile = 0;
    QFile *rawFile = 0;

    if (source.getExtension().toLower() == sessionExt) {
        bzFile = new BZipFileDevice(source.getLocalFilename());
        if (!bzFile->open(QIODevice::ReadOnly)) {
            delete bzFile;
            return FileOpenFailed;
        }
        inputSource = new QXmlInputSource(bzFile);
    } else {
        rawFile = new QFile(source.getLocalFilename());
        inputSource = new QXmlInputSource(rawFile);
    }

    if (!checkSaveModified()) {
        if (bzFile) bzFile->close();
        delete inputSource;
        delete bzFile;
        delete rawFile;
        return FileOpenCancelled;
    }

    QString error;
    closeSession();
    createDocument();

    PaneCallback callback(this);
    m_viewManager->clearSelections();

    SVFileReader reader(m_document, callback, source.getLocation());
    connect
        (&reader, SIGNAL(modelRegenerationFailed(QString, QString, QString)),
         this, SLOT(modelRegenerationFailed(QString, QString, QString)));
    connect
        (&reader, SIGNAL(modelRegenerationWarning(QString, QString, QString)),
         this, SLOT(modelRegenerationWarning(QString, QString, QString)));

    reader.parse(*inputSource);
    
    if (!reader.isOK()) {
        error = tr("SV XML file read error:\n%1").arg(reader.getErrorString());
    }
    
    if (bzFile) bzFile->close();

    delete inputSource;
    delete bzFile;
    delete rawFile;

    bool ok = (error == "");

    if (ok) {

        emit activity(tr("Import session file \"%1\"").arg(source.getLocation()));

	setWindowTitle(tr("%1: %2")
                       .arg(QApplication::applicationName())
		       .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());
        }

        emit sessionLoaded();

    } else {
	setWindowTitle(QApplication::applicationName());
    }

    return ok ? FileOpenSucceeded : FileOpenFailed;
}

MainWindowBase::FileOpenStatus
MainWindowBase::openSessionTemplate(QString templateName)
{
    // Template in the user's template directory takes
    // priority over a bundled one; we don't unbundle, but
    // open directly from the bundled file (where applicable)
    ResourceFinder rf;
    QString tfile = rf.getResourcePath("templates", templateName + ".svt");
    if (tfile != "") {
        cerr << "SV loading template file " << tfile << endl;
        return openSessionTemplate(FileSource("file:" + tfile));
    } else {
        return FileOpenFailed;
    }
}

MainWindowBase::FileOpenStatus
MainWindowBase::openSessionTemplate(FileSource source)
{
    cerr << "MainWindowBase::openSessionTemplate(" << source.getLocation() << ")" << endl;

    if (!source.isAvailable()) return FileOpenFailed;
    source.waitForData();

    QXmlInputSource *inputSource = 0;
    QFile *file = 0;

    file = new QFile(source.getLocalFilename());
    inputSource = new QXmlInputSource(file);

    if (!checkSaveModified()) {
        delete inputSource;
        delete file;
        return FileOpenCancelled;
    }

    QString error;
    closeSession();
    createDocument();

    PaneCallback callback(this);
    m_viewManager->clearSelections();

    SVFileReader reader(m_document, callback, source.getLocation());
    connect
        (&reader, SIGNAL(modelRegenerationFailed(QString, QString, QString)),
         this, SLOT(modelRegenerationFailed(QString, QString, QString)));
    connect
        (&reader, SIGNAL(modelRegenerationWarning(QString, QString, QString)),
         this, SLOT(modelRegenerationWarning(QString, QString, QString)));

    reader.parse(*inputSource);
    
    if (!reader.isOK()) {
        error = tr("SV XML file read error:\n%1").arg(reader.getErrorString());
    }
    
    delete inputSource;
    delete file;

    bool ok = (error == "");

    setWindowTitle(QApplication::applicationName());

    if (ok) {

        emit activity(tr("Open session template \"%1\"").arg(source.getLocation()));

	setupMenus();

	CommandHistory::getInstance()->clear();
	CommandHistory::getInstance()->documentSaved();
	m_documentModified = false;
	updateMenuStates();

        emit sessionLoaded();
    }

    return ok ? FileOpenSucceeded : FileOpenFailed;
}

MainWindowBase::FileOpenStatus
MainWindowBase::openSessionFromRDF(FileSource source)
{
    SVDEBUG << "MainWindowBase::openSessionFromRDF(" << source.getLocation() << ")" << endl;

    if (!source.isAvailable()) return FileOpenFailed;
    source.waitForData();

    if (!checkSaveModified()) {
        return FileOpenCancelled;
    }
    
    closeSession();
    createDocument();

    FileOpenStatus status = openLayersFromRDF(source);

    setupMenus();
    
    setWindowTitle(tr("%1: %2")
                   .arg(QApplication::applicationName())
                   .arg(source.getLocation()));
    CommandHistory::getInstance()->clear();
    CommandHistory::getInstance()->documentSaved();
    m_documentModified = false;

    emit sessionLoaded();

    return status;
}

MainWindowBase::FileOpenStatus
MainWindowBase::openLayersFromRDF(FileSource source)
{
    sv_samplerate_t rate = 0;

    SVDEBUG << "MainWindowBase::openLayersFromRDF" << endl;

    ProgressDialog dialog(tr("Importing from RDF..."), true, 2000, this);
    connect(&dialog, SIGNAL(showing()), this, SIGNAL(hideSplash()));

    if (getMainModel()) {
        rate = getMainModel()->getSampleRate();
    } else if (Preferences::getInstance()->getResampleOnLoad()) {
        if (getMainModel()) {
            rate = getMainModel()->getSampleRate();
        }
    }

    RDFImporter importer
        (QUrl::fromLocalFile(source.getLocalFilename()).toString(), rate);

    if (!importer.isOK()) {
        if (importer.getErrorString() != "") {
            QMessageBox::critical
                (this, tr("Failed to import RDF"),
                 tr("<b>Failed to import RDF</b><p>Importing data from RDF document at \"%1\" failed: %2</p>")
                 .arg(source.getLocation()).arg(importer.getErrorString()));
        }
        return FileOpenFailed;
    }

    std::vector<Model *> models = importer.getDataModels(&dialog);

    dialog.setMessage(tr("Importing from RDF..."));

    if (models.empty()) {
        QMessageBox::critical
            (this, tr("Failed to import RDF"),
             tr("<b>Failed to import RDF</b><p>No suitable data models found for import from RDF document at \"%1\"</p>").arg(source.getLocation()));
        return FileOpenFailed;
    }

    emit activity(tr("Import RDF document \"%1\"").arg(source.getLocation()));

    std::set<Model *> added;

    for (int i = 0; i < (int)models.size(); ++i) {

        Model *m = models[i];
        WaveFileModel *w = dynamic_cast<WaveFileModel *>(m);

        if (w) {

            Pane *pane = addPaneToStack();
            Layer *layer = 0;

            if (m_timeRulerLayer) {
                m_document->addLayerToView(pane, m_timeRulerLayer);
            }

            if (!getMainModel()) {
                m_document->setMainModel(w);
                layer = m_document->createMainModelLayer(LayerFactory::Waveform);
            } else {
                layer = m_document->createImportedLayer(w);
            }

            m_document->addLayerToView(pane, layer);

            added.insert(w);
            
            for (int j = 0; j < (int)models.size(); ++j) {

                Model *dm = models[j];

                if (dm == m) continue;
                if (dm->getSourceModel() != m) continue;

                layer = m_document->createImportedLayer(dm);

                if (layer->isLayerOpaque() ||
                    dynamic_cast<Colour3DPlotLayer *>(layer)) {

                    // these always go in a new pane, with nothing
                    // else going in the same pane

                    Pane *singleLayerPane = addPaneToStack();
                    if (m_timeRulerLayer) {
                        m_document->addLayerToView(singleLayerPane, m_timeRulerLayer);
                    }
                    m_document->addLayerToView(singleLayerPane, layer);

                } else if (layer->getLayerColourSignificance() ==
                           Layer::ColourHasMeaningfulValue) {

                    // these can go in a pane with something else, but
                    // only if none of the something elses also have
                    // this quality

                    bool needNewPane = false;
                    for (int i = 0; i < pane->getLayerCount(); ++i) {
                        Layer *otherLayer = pane->getLayer(i);
                        if (otherLayer &&
                            (otherLayer->getLayerColourSignificance() ==
                             Layer::ColourHasMeaningfulValue)) {
                            needNewPane = true;
                            break;
                        }
                    }
                    if (needNewPane) {
                        pane = addPaneToStack();
                    }

                    m_document->addLayerToView(pane, layer);

                } else {

                    if (pane->getLayerCount() > 4) {
                        pane = addPaneToStack();
                    }

                    m_document->addLayerToView(pane, layer);
                }

                added.insert(dm);
            }
        }
    }

    for (int i = 0; i < (int)models.size(); ++i) {

        Model *m = models[i];

        if (added.find(m) == added.end()) {
            
            Layer *layer = m_document->createImportedLayer(m);
            if (!layer) return FileOpenFailed;

            Pane *singleLayerPane = addPaneToStack();
            if (m_timeRulerLayer) {
                m_document->addLayerToView(singleLayerPane, m_timeRulerLayer);
            }
            m_document->addLayerToView(singleLayerPane, layer);
        }
    }
            
    m_recentFiles.addFile(source.getLocation());
    return FileOpenSucceeded;
}

void
MainWindowBase::createAudioIO()
{
    if (m_playTarget || m_audioIO) return;

    if (!(m_soundOptions & WithAudioOutput)) return;

    QSettings settings;
    settings.beginGroup("Preferences");
    QString implementation = settings.value
        ("audio-target", "").toString();
    QString suffix;
    if (implementation != "") suffix = "-" + implementation;
    QString recordDevice = settings.value
        ("audio-record-device" + suffix, "").toString();
    QString playbackDevice = settings.value
        ("audio-playback-device" + suffix, "").toString();
    settings.endGroup();

    if (implementation == "auto") {
        implementation = "";
    }
    
    breakfastquay::AudioFactory::Preference preference;
    preference.implementation = implementation.toStdString();
    preference.recordDevice = recordDevice.toStdString();
    preference.playbackDevice = playbackDevice.toStdString();

    SVCERR << "createAudioIO: Preferred implementation = \""
            << preference.implementation << "\"" << endl;
    SVCERR << "createAudioIO: Preferred playback device = \""
            << preference.playbackDevice << "\"" << endl;
    SVCERR << "createAudioIO: Preferred record device = \""
            << preference.recordDevice << "\"" << endl;

    if (!m_resamplerWrapper) {
        m_resamplerWrapper = new breakfastquay::ResamplerWrapper(m_playSource);
        m_playSource->setResamplerWrapper(m_resamplerWrapper);
    }

    std::string errorString;
    
    if (m_soundOptions & WithAudioInput) {
        m_audioIO = breakfastquay::AudioFactory::
            createCallbackIO(m_recordTarget, m_resamplerWrapper,
                             preference, errorString);
        if (m_audioIO) {
            m_audioIO->suspend(); // start in suspended state
            m_playSource->setSystemPlaybackTarget(m_audioIO);
        }
    } else {
        m_playTarget = breakfastquay::AudioFactory::
            createCallbackPlayTarget(m_resamplerWrapper,
                                     preference, errorString);
        if (m_playTarget) {
            m_playTarget->suspend(); // start in suspended state
            m_playSource->setSystemPlaybackTarget(m_playTarget);
        }
    }

    if (!m_playTarget && !m_audioIO) {
        emit hideSplash();
        QString message;
        QString error = errorString.c_str();
        QString firstBit, secondBit;
        if (implementation == "") {
            if (error == "") {
                firstBit = tr("<b>No audio available</b><p>Could not open an audio device.</p>");
            } else {
                firstBit = tr("<b>No audio available</b><p>Could not open audio device: %1</p>").arg(error);
            }
            if (m_soundOptions & WithAudioInput) {
                secondBit = tr("<p>Automatic audio device detection failed. Audio playback and recording will not be available during this session.</p>");
            } else {
                secondBit = tr("<p>Automatic audio device detection failed. Audio playback will not be available during this session.</p>");
            }
        } else {
            QString driverName = breakfastquay::AudioFactory::
                getImplementationDescription(implementation.toStdString())
                .c_str();
            if (error == "") {
                firstBit = tr("<b>No audio available</b><p>Failed to open your preferred audio driver (\"%1\").</p>").arg(driverName);
            } else {
                firstBit = tr("<b>No audio available</b><p>Failed to open your preferred audio driver (\"%1\"): %2.</p>").arg(driverName).arg(error);
            }
            if (m_soundOptions & WithAudioInput) {
                secondBit = tr("<p>Audio playback and recording will not be available during this session.</p>");
            } else {
                secondBit = tr("<p>Audio playback will not be available during this session.</p>");
            }
        }
        SVDEBUG << "createAudioIO: ERROR: Failed to open audio device \""
                << implementation << "\": error is: " << error << endl;
        QMessageBox::warning(this, tr("Couldn't open audio device"),
                             firstBit + secondBit, QMessageBox::Ok);
    }
}

void
MainWindowBase::deleteAudioIO()
{
    // First prevent this trying to call target.
    if (m_playSource) {
        m_playSource->setSystemPlaybackTarget(0);
        m_playSource->setResamplerWrapper(0);
    }

    // Then delete the breakfastquay::System object.
    // Only one of these two exists!
    delete m_audioIO;
    delete m_playTarget;

    // And the breakfastquay resampler wrapper. We need to
    // delete/recreate this if the channel count changes, which is one
    // of the use cases for recreateAudioIO() calling this
    delete m_resamplerWrapper;

    m_audioIO = 0;
    m_playTarget = 0;
    m_resamplerWrapper = 0;
}

void
MainWindowBase::recreateAudioIO()
{
    deleteAudioIO();
    createAudioIO();
}

void
MainWindowBase::audioChannelCountIncreased(int)
{
    recreateAudioIO();
}

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, QString)),
            this, SLOT(modelGenerationFailed(QString, QString)));
    connect(m_document, SIGNAL(modelRegenerationWarning(QString, QString, QString)),
            this, SLOT(modelRegenerationWarning(QString, QString, QString)));
    connect(m_document, SIGNAL(modelGenerationFailed(QString, QString)),
            this, SLOT(modelGenerationFailed(QString, QString)));
    connect(m_document, SIGNAL(modelRegenerationWarning(QString, QString, QString)),
            this, SLOT(modelRegenerationWarning(QString, QString, QString)));
    connect(m_document, SIGNAL(alignmentComplete(AlignmentModel *)),
            this, SLOT(alignmentComplete(AlignmentModel *)));
    connect(m_document, SIGNAL(alignmentFailed(QString)),
            this, SLOT(alignmentFailed(QString)));

    emit replacedDocument();
}

bool
MainWindowBase::saveSessionFile(QString path)
{
    try {

        TempWriteFile temp(path);

        BZipFileDevice bzFile(temp.getTemporaryFilename());
        if (!bzFile.open(QIODevice::WriteOnly)) {
            cerr << "Failed to open session file \""
                      << temp.getTemporaryFilename()
                      << "\" for writing: "
                      << bzFile.errorString() << endl;
            return false;
        }

        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

        QTextStream out(&bzFile);
        out.setCodec(QTextCodec::codecForName("UTF-8"));
        toXml(out, false);
        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();
        temp.moveToTarget();
        return true;

    } catch (FileOperationFailed &f) {
        
        QMessageBox::critical(this, tr("Failed to write file"),
                              tr("<b>Save failed</b><p>Failed to write to file \"%1\": %2")
                              .arg(path).arg(f.what()));
        return false;
    }
}

bool
MainWindowBase::saveSessionTemplate(QString path)
{
    try {

        TempWriteFile temp(path);

        QFile file(temp.getTemporaryFilename());
        if (!file.open(QIODevice::WriteOnly)) {
            cerr << "Failed to open session template file \""
                      << temp.getTemporaryFilename()
                      << "\" for writing: "
                      << file.errorString() << endl;
            return false;
        }
        
        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

        QTextStream out(&file);
        out.setCodec(QTextCodec::codecForName("UTF-8"));
        toXml(out, true);
        out.flush();

        QApplication::restoreOverrideCursor();

        file.close();
        temp.moveToTarget();
        return true;

    } catch (FileOperationFailed &f) {
        
        QMessageBox::critical(this, tr("Failed to write file"),
                              tr("<b>Save failed</b><p>Failed to write to file \"%1\": %2")
                              .arg(path).arg(f.what()));
        return false;
    }
}

void
MainWindowBase::toXml(QTextStream &out, bool asTemplate)
{
    QString indent("  ");

    out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    out << "<!DOCTYPE sonic-visualiser>\n";
    out << "<sv>\n";

    if (asTemplate) {
        m_document->toXmlAsTemplate(out, "", "");
    } else {
        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()
{
    cerr << "MainWindowBase::addPaneToStack()" << endl;
    AddPaneCommand *command = new AddPaneCommand(this);
    CommandHistory::getInstance()->addCommand(command);
    Pane *pane = command->getPane();
    return pane;
}

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;
    
    sv_frame_t start = model->getStartFrame();
    sv_frame_t end = model->getEndFrame();
    if (m_playSource) end = std::max(end, m_playSource->getPlayEndFrame());
    int pixels = currentPane->width();

    int sw = currentPane->getVerticalScaleWidth();
    if (pixels > sw * 2) pixels -= sw * 2;
    else pixels = 1;
    if (pixels > 4) pixels -= 4;

    int zoomLevel = int((end - start) / pixels);
    if (zoomLevel < 1) zoomLevel = 1;

    currentPane->setZoomLevel(zoomLevel);
    currentPane->setCentreFrame((start + end) / 2);
}

void
MainWindowBase::zoomDefault()
{
    Pane *currentPane = m_paneStack->getCurrentPane();
    QSettings settings;
    settings.beginGroup("MainWindow");
    int zoom = settings.value("zoom-default", 1024).toInt();
    settings.endGroup();
    if (currentPane) currentPane->setZoomLevel(zoom);
}

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::peekLeft()
{
    Pane *currentPane = m_paneStack->getCurrentPane();
    if (currentPane) currentPane->scroll(false, false, false);
}

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::peekRight()
{
    Pane *currentPane = m_paneStack->getCurrentPane();
    if (currentPane) currentPane->scroll(true, false, false);
}

void
MainWindowBase::showNoOverlays()
{
    m_viewManager->setOverlayMode(ViewManager::NoOverlays);
}

void
MainWindowBase::showMinimalOverlays()
{
    m_viewManager->setOverlayMode(ViewManager::StandardOverlays);
}

void
MainWindowBase::showAllOverlays()
{
    m_viewManager->setOverlayMode(ViewManager::AllOverlays);
}

void
MainWindowBase::toggleTimeRulers()
{
    bool haveRulers = false;
    bool someHidden = 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 *layer = pane->getLayer(j);
            if (!dynamic_cast<TimeRulerLayer *>(layer)) continue;

            haveRulers = true;
            if (layer->isLayerDormant(pane)) someHidden = true;
        }
    }

    if (haveRulers) {

        bool show = someHidden;

        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 *layer = pane->getLayer(j);
                if (!dynamic_cast<TimeRulerLayer *>(layer)) continue;

                layer->showLayer(pane, show);
            }
        }
    }
}

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);
    }
}

QLabel *
MainWindowBase::getStatusLabel() const
{
    if (!m_statusLabel) {
        m_statusLabel = new QLabel();
        statusBar()->addWidget(m_statusLabel, 1);
    }

    QList<QFrame *> frames = statusBar()->findChildren<QFrame *>();
    foreach (QFrame *f, frames) {
        f->setFrameStyle(QFrame::NoFrame);
    }

    return m_statusLabel;
}

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::toggleCentreLine()
{
    if (m_viewManager->shouldShowCentreLine()) {
        m_viewManager->setShowCentreLine(false);
    } else {
        m_viewManager->setShowCentreLine(true);
    }
}

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_recordTarget && m_recordTarget->isRecording()) ||
        (m_playSource && m_playSource->isPlaying())) {
        stop();
        QAction *action = qobject_cast<QAction *>(sender());
        if (action) action->setChecked(false);
    } else {
        if (m_audioIO) m_audioIO->resume();
        else if (m_playTarget) m_playTarget->resume();
        playbackFrameChanged(m_viewManager->getPlaybackFrame());
	m_playSource->play(m_viewManager->getPlaybackFrame());
    }
}

void
MainWindowBase::record()
{
    if (!(m_soundOptions & WithAudioInput)) {
        return;
    }

    if (!m_recordTarget) {
        //!!! report
        return;
    }

    if (!m_audioIO) {
        cerr << "MainWindowBase::record: about to create audio IO" << endl;
        createAudioIO();
    }

    if (!m_audioIO) {
        // don't need to report this, createAudioIO already should have
        return;
    }
    
    if (m_recordTarget->isRecording()) {
        stop();
        return;
    }

    QAction *action = qobject_cast<QAction *>(sender());
    
    if (m_audioRecordMode == RecordReplaceSession) {
        if (!checkSaveModified()) {
            if (action) action->setChecked(false);
            return;
        }
    }

    if (m_viewManager) m_viewManager->setGlobalCentreFrame(0);
    
    cerr << "MainWindowBase::record: about to resume" << endl;
    m_audioIO->resume();

    WritableWaveFileModel *model = m_recordTarget->startRecording();
    if (!model) {
        cerr << "ERROR: MainWindowBase::record: Recording failed" << endl;
        //!!! report
        if (action) action->setChecked(false);
        return;
    }

    if (!model->isOK()) {
        m_recordTarget->stopRecording();
        m_audioIO->suspend();
        delete model;
        return;
    }
    
    PlayParameterRepository::getInstance()->addPlayable(model);

    if (m_audioRecordMode == RecordReplaceSession || !getMainModel()) {

        //!!! duplication with openAudio here
        
        QString templateName = getDefaultSessionTemplate();
        bool loadedTemplate = false;
        
        if (templateName != "") {
            FileOpenStatus tplStatus = openSessionTemplate(templateName);
            if (tplStatus == FileOpenCancelled) {
                m_recordTarget->stopRecording();
                m_audioIO->suspend();
                PlayParameterRepository::getInstance()->removePlayable(model);
                return;
            }
            if (tplStatus != FileOpenFailed) {
                loadedTemplate = true;
            }
        }

        if (!loadedTemplate) {
            closeSession();
            createDocument();
        }
        
        Model *prevMain = getMainModel();
        if (prevMain) {
            m_playSource->removeModel(prevMain);
            PlayParameterRepository::getInstance()->removePlayable(prevMain);
        }
        
        m_document->setMainModel(model);
        setupMenus();

	if (loadedTemplate || (m_sessionFile == "")) {
            //!!! shouldn't be dealing directly with title from here -- call a method
	    setWindowTitle(tr("%1: %2")
                           .arg(QApplication::applicationName())
                           .arg(model->getLocation()));
	    CommandHistory::getInstance()->clear();
	    CommandHistory::getInstance()->documentSaved();
	    m_documentModified = false;
	} else {
	    setWindowTitle(tr("%1: %2 [%3]")
                           .arg(QApplication::applicationName())
			   .arg(QFileInfo(m_sessionFile).fileName())
			   .arg(model->getLocation()));
	    if (m_documentModified) {
		m_documentModified = false;
		documentModified(); // so as to restore "(modified)" window title
	    }
	}

    } else {

        CommandHistory::getInstance()->startCompoundOperation
            (tr("Import Recorded Audio"), true);

        m_document->addImportedModel(model);

        AddPaneCommand *command = new AddPaneCommand(this);
        CommandHistory::getInstance()->addCommand(command);

        Pane *pane = command->getPane();

        if (m_timeRulerLayer) {
            m_document->addLayerToView(pane, m_timeRulerLayer);
        }

        Layer *newLayer = m_document->createImportedLayer(model);

        if (newLayer) {
            m_document->addLayerToView(pane, newLayer);
        }
	
        CommandHistory::getInstance()->endCompoundOperation();
    }

    updateMenuStates();
    m_recentFiles.addFile(model->getLocation());
    currentPaneChanged(m_paneStack->getCurrentPane());

    emit audioFileLoaded();
}

void
MainWindowBase::ffwd()
{
    if (!getMainModel()) return;

    sv_frame_t frame = m_viewManager->getPlaybackFrame();
    ++frame;

    Pane *pane = m_paneStack->getCurrentPane();
    Layer *layer = getSnapLayer();
    sv_samplerate_t sr = getMainModel()->getSampleRate();

    if (!layer) {

        frame = RealTime::realTime2Frame
            (RealTime::frame2RealTime(frame, sr) + m_defaultFfwdRwdStep, sr);
        if (frame > getMainModel()->getEndFrame()) {
            frame = getMainModel()->getEndFrame();
        }

    } else {

        int resolution = 0;
        if (pane) frame = pane->alignFromReference(frame);
        if (layer->snapToFeatureFrame(m_paneStack->getCurrentPane(),
                                      frame, resolution, Layer::SnapRight)) {
            if (pane) frame = pane->alignToReference(frame);
        } else {
            frame = getMainModel()->getEndFrame();
        }
    }
        
    if (frame < 0) frame = 0;

    if (m_viewManager->getPlaySelectionMode()) {
        frame = m_viewManager->constrainFrameToSelection(frame);
    }
    
    m_viewManager->setPlaybackFrame(frame);

    if (frame == getMainModel()->getEndFrame() &&
        m_playSource &&
        m_playSource->isPlaying() &&
        !m_viewManager->getPlayLoopMode()) {
        stop();
    }
}

void
MainWindowBase::ffwdEnd()
{
    if (!getMainModel()) return;

    if (m_playSource &&
        m_playSource->isPlaying() &&
        !m_viewManager->getPlayLoopMode()) {
        stop();
    }

    sv_frame_t frame = getMainModel()->getEndFrame();

    if (m_viewManager->getPlaySelectionMode()) {
        frame = m_viewManager->constrainFrameToSelection(frame);
    }

    m_viewManager->setPlaybackFrame(frame);
}

void
MainWindowBase::ffwdSimilar()
{
    if (!getMainModel()) return;

    Layer *layer = getSnapLayer();
    if (!layer) { ffwd(); return; }

    Pane *pane = m_paneStack->getCurrentPane();
    sv_frame_t frame = m_viewManager->getPlaybackFrame();

    int resolution = 0;
    if (pane) frame = pane->alignFromReference(frame);
    if (layer->snapToSimilarFeature(m_paneStack->getCurrentPane(),
                                    frame, resolution, Layer::SnapRight)) {
        if (pane) frame = pane->alignToReference(frame);
    } else {
        frame = getMainModel()->getEndFrame();
    }
        
    if (frame < 0) frame = 0;

    if (m_viewManager->getPlaySelectionMode()) {
        frame = m_viewManager->constrainFrameToSelection(frame);
    }
    
    m_viewManager->setPlaybackFrame(frame);

    if (frame == getMainModel()->getEndFrame() &&
        m_playSource &&
        m_playSource->isPlaying() &&
        !m_viewManager->getPlayLoopMode()) {
        stop();
    }
}

void
MainWindowBase::rewind()
{
    if (!getMainModel()) return;

    sv_frame_t frame = m_viewManager->getPlaybackFrame();
    if (frame > 0) --frame;

    Pane *pane = m_paneStack->getCurrentPane();
    Layer *layer = getSnapLayer();
    sv_samplerate_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.15);
        if (ct < RealTime::zeroTime) ct = RealTime::zeroTime;
        frame = RealTime::realTime2Frame(ct, sr);
    }

    if (!layer) {
        
        frame = RealTime::realTime2Frame
            (RealTime::frame2RealTime(frame, sr) - m_defaultFfwdRwdStep, sr);
        if (frame < getMainModel()->getStartFrame()) {
            frame = getMainModel()->getStartFrame();
        }

    } else {

        int resolution = 0;
        if (pane) frame = pane->alignFromReference(frame);
        if (layer->snapToFeatureFrame(m_paneStack->getCurrentPane(),
                                      frame, resolution, Layer::SnapLeft)) {
            if (pane) frame = pane->alignToReference(frame);
        } else {
            frame = getMainModel()->getStartFrame();
        }
    }

    if (frame < 0) frame = 0;

    if (m_viewManager->getPlaySelectionMode()) {
        frame = m_viewManager->constrainFrameToSelection(frame);
    }

    m_viewManager->setPlaybackFrame(frame);
}

void
MainWindowBase::rewindStart()
{
    if (!getMainModel()) return;

    sv_frame_t frame = getMainModel()->getStartFrame();

    if (m_viewManager->getPlaySelectionMode()) {
        frame = m_viewManager->constrainFrameToSelection(frame);
    }

    m_viewManager->setPlaybackFrame(frame);
}

void
MainWindowBase::rewindSimilar()
{
    if (!getMainModel()) return;

    Layer *layer = getSnapLayer();
    if (!layer) { rewind(); return; }

    Pane *pane = m_paneStack->getCurrentPane();
    sv_frame_t frame = m_viewManager->getPlaybackFrame();

    int resolution = 0;
    if (pane) frame = pane->alignFromReference(frame);
    if (layer->snapToSimilarFeature(m_paneStack->getCurrentPane(),
                                    frame, resolution, Layer::SnapLeft)) {
        if (pane) frame = pane->alignToReference(frame);
    } else {
        frame = getMainModel()->getStartFrame();
    }
        
    if (frame < 0) frame = 0;

    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<RegionLayer *>(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()
{
    if (m_recordTarget &&
        m_recordTarget->isRecording()) {
        m_recordTarget->stopRecording();
    }

    if (!m_playSource) return;
    
    m_playSource->stop();

    if (m_audioIO) m_audioIO->suspend();
    else if (m_playTarget) m_playTarget->suspend();
    
    if (m_paneStack && m_paneStack->getCurrentPane()) {
        updateVisibleRangeDisplay(m_paneStack->getCurrentPane());
    } else {
        m_myStatusMessage = "";
        getStatusLabel()->setText("");
    }
}

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_prevCurrentPane(0),
    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::editCurrentLayer()
{
    Layer *layer = 0;
    Pane *pane = m_paneStack->getCurrentPane();
    if (pane) layer = pane->getSelectedLayer();
    if (!layer) return;

    Model *model = layer->getModel();
    if (!model) return;

    TabularModel *tabular = dynamic_cast<TabularModel *>(model);
    if (!tabular) {
        //!!! how to prevent this function from being active if not
        //appropriate model type?  or will we ultimately support
        //tabular display for all editable models?
        SVDEBUG << "NOTE: Not a tabular model" << endl;
        return;
    }

    if (m_layerDataDialogMap.find(layer) != m_layerDataDialogMap.end()) {
        if (!m_layerDataDialogMap[layer].isNull()) {
            m_layerDataDialogMap[layer]->show();
            m_layerDataDialogMap[layer]->raise();
            return;
        }
    }

    QString title = layer->getLayerPresentationName();

    ModelDataTableDialog *dialog = new ModelDataTableDialog(tabular, title, this);
    dialog->setAttribute(Qt::WA_DeleteOnClose);
    
    connectLayerEditDialog(dialog);

    m_layerDataDialogMap[layer] = dialog;
    m_viewDataDialogMap[pane].insert(dialog);

    dialog->show();
}

void
MainWindowBase::connectLayerEditDialog(ModelDataTableDialog *dialog)
{
    connect(m_viewManager,
            SIGNAL(globalCentreFrameChanged(sv_frame_t)),
            dialog,
            SLOT(userScrolledToFrame(sv_frame_t)));

    connect(m_viewManager,
            SIGNAL(playbackFrameChanged(sv_frame_t)),
            dialog,
            SLOT(playbackScrolledToFrame(sv_frame_t)));

    connect(dialog,
            SIGNAL(scrollToFrame(sv_frame_t)),
            m_viewManager,
            SLOT(setGlobalCentreFrame(sv_frame_t)));

    connect(dialog,
            SIGNAL(scrollToFrame(sv_frame_t)),
            m_viewManager,
            SLOT(setPlaybackFrame(sv_frame_t)));
}    

void
MainWindowBase::previousPane()
{
    if (!m_paneStack) return;

    Pane *currentPane = m_paneStack->getCurrentPane();
    if (!currentPane) return;

    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
        if (m_paneStack->getPane(i) == currentPane) {
            if (i == 0) return;
            m_paneStack->setCurrentPane(m_paneStack->getPane(i-1));
            updateMenuStates();
            return;
        }
    }
}

void
MainWindowBase::nextPane()
{
    if (!m_paneStack) return;

    Pane *currentPane = m_paneStack->getCurrentPane();
    if (!currentPane) return;

    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
        if (m_paneStack->getPane(i) == currentPane) {
            if (i == m_paneStack->getPaneCount()-1) return;
            m_paneStack->setCurrentPane(m_paneStack->getPane(i+1));
            updateMenuStates();
            return;
        }
    }
}

void
MainWindowBase::previousLayer()
{
    if (!m_paneStack) return;

    Pane *currentPane = m_paneStack->getCurrentPane();
    if (!currentPane) return;

    int count = currentPane->getLayerCount();
    if (count == 0) return;

    Layer *currentLayer = currentPane->getSelectedLayer();

    if (!currentLayer) {
        // The pane itself is current
        m_paneStack->setCurrentLayer
            (currentPane, currentPane->getFixedOrderLayer(count-1));
    } else {
        for (int i = 0; i < count; ++i) {
            if (currentPane->getFixedOrderLayer(i) == currentLayer) {
                if (i == 0) {
                    m_paneStack->setCurrentLayer
                        (currentPane, 0); // pane
                } else {
                    m_paneStack->setCurrentLayer
                        (currentPane, currentPane->getFixedOrderLayer(i-1));
                }
                break;
            }
        }
    }

    updateMenuStates();
}

void
MainWindowBase::nextLayer()
{
    if (!m_paneStack) return;

    Pane *currentPane = m_paneStack->getCurrentPane();
    if (!currentPane) return;

    int count = currentPane->getLayerCount();
    if (count == 0) return;

    Layer *currentLayer = currentPane->getSelectedLayer();

    if (!currentLayer) {
        // The pane itself is current
        m_paneStack->setCurrentLayer
            (currentPane, currentPane->getFixedOrderLayer(0));
    } else {
        for (int i = 0; i < count; ++i) {
            if (currentPane->getFixedOrderLayer(i) == currentLayer) {
                if (i == currentPane->getLayerCount()-1) {
                    m_paneStack->setCurrentLayer
                        (currentPane, 0); // pane
                } else {
                    m_paneStack->setCurrentLayer
                        (currentPane, currentPane->getFixedOrderLayer(i+1));
                }
                break;
            }
        }
    }

    updateMenuStates();
}

void
MainWindowBase::playbackFrameChanged(sv_frame_t frame)
{
    if (!(m_playSource && m_playSource->isPlaying()) || !getMainModel()) return;

    updatePositionStatusDisplays();

    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);

    getStatusLabel()->setText(m_myStatusMessage);
}

void
MainWindowBase::recordDurationChanged(sv_frame_t frame, sv_samplerate_t rate)
{
    RealTime duration = RealTime::frame2RealTime(frame, rate);
    QString durStr = duration.toSecText().c_str();
    
    m_myStatusMessage = tr("Recording: %1").arg(durStr);

    getStatusLabel()->setText(m_myStatusMessage);
}

void
MainWindowBase::globalCentreFrameChanged(sv_frame_t )
{
    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, sv_frame_t frame)
{
//    SVDEBUG << "MainWindowBase::viewCentreFrameChanged(" << v << "," << frame << ")" << endl;

    if (m_viewDataDialogMap.find(v) != m_viewDataDialogMap.end()) {
        for (DataDialogSet::iterator i = m_viewDataDialogMap[v].begin();
             i != m_viewDataDialogMap[v].end(); ++i) {
            (*i)->userScrolledToFrame(frame);
        }
    }
    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, int , 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 *)
{
//    SVDEBUG << "MainWindowBase::layerAdded(" << layer << ")" << endl;
    updateMenuStates();
}

void
MainWindowBase::layerRemoved(Layer *)
{
//    SVDEBUG << "MainWindowBase::layerRemoved(" << layer << ")" << endl;
    updateMenuStates();
}

void
MainWindowBase::layerAboutToBeDeleted(Layer *layer)
{
//    SVDEBUG << "MainWindowBase::layerAboutToBeDeleted(" << layer << ")" << endl;

    removeLayerEditDialog(layer);

    if (m_timeRulerLayer && (layer == m_timeRulerLayer)) {
//	cerr << "(this is the time ruler layer)" << endl;
	m_timeRulerLayer = 0;
    }
}

void
MainWindowBase::layerInAView(Layer *layer, bool inAView)
{
//    SVDEBUG << "MainWindowBase::layerInAView(" << layer << "," << inAView << ")" << endl;

    if (!inAView) removeLayerEditDialog(layer);

    // 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 &&
                        !dynamic_cast<TimeRulerLayer *>(pl) &&
                        (pl->getModel() == model)) {
                        found = true;
                        break;
                    }
                }
                if (found) break;
            }
            if (!found) {
                m_playSource->removeModel(model);
            }
        }
    }

    updateMenuStates();
}

void
MainWindowBase::removeLayerEditDialog(Layer *layer)
{
    if (m_layerDataDialogMap.find(layer) != m_layerDataDialogMap.end()) {

        ModelDataTableDialog *dialog = m_layerDataDialogMap[layer];

        for (ViewDataDialogMap::iterator vi = m_viewDataDialogMap.begin();
             vi != m_viewDataDialogMap.end(); ++vi) {
            vi->second.erase(dialog);
        }

        m_layerDataDialogMap.erase(layer);
        delete dialog;
    }
}

void
MainWindowBase::modelAdded(Model *model)
{
//    SVDEBUG << "MainWindowBase::modelAdded(" << model << ")" << endl;
	std::cerr << "\nAdding model " << model->getTypeName() << " to playsource " << std::endl;
    m_playSource->addModel(model);
}

void
MainWindowBase::mainModelChanged(WaveFileModel *model)
{
//    SVDEBUG << "MainWindowBase::mainModelChanged(" << model << ")" << endl;
    updateDescriptionLabel();
    if (model) m_viewManager->setMainModelSampleRate(model->getSampleRate());
    if (model && !(m_playTarget || m_audioIO) &&
        (m_soundOptions & WithAudioOutput)) {
        createAudioIO();
    }
}

void
MainWindowBase::modelAboutToBeDeleted(Model *model)
{
//    SVDEBUG << "MainWindowBase::modelAboutToBeDeleted(" << model << ")" << endl;
    if (model == m_viewManager->getPlaybackModel()) {
        m_viewManager->setPlaybackModel(0);
    }
    m_playSource->removeModel(model);
}

void
MainWindowBase::paneDeleteButtonClicked(Pane *pane)
{
    bool found = false;
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
        if (m_paneStack->getPane(i) == pane) {
            found = true;
            break;
        }
    }
    if (!found) {
        SVDEBUG << "MainWindowBase::paneDeleteButtonClicked: Unknown pane "
                  << pane << endl;
        return;
    }

    CommandHistory::getInstance()->startCompoundOperation
	(tr("Delete Pane"), true);

    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::alignmentComplete(AlignmentModel *model)
{
    cerr << "MainWindowBase::alignmentComplete(" << model << ")" << endl;
}

void
MainWindowBase::pollOSC()
{
    if (!m_oscQueue || m_oscQueue->isEmpty()) return;
    SVDEBUG << "MainWindowBase::pollOSC: have " << m_oscQueue->getMessagesAvailable() << " messages" << 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) {
        //cerr << "JTEST: mouse event on selection pane" << endl;
        updateVisibleRangeDisplay(currentPane);
    }
}

void
MainWindowBase::contextHelpChanged(const QString &s)
{
    QLabel *lab = getStatusLabel();

    if (s == "" && m_myStatusMessage != "") {
        if (lab->text() != m_myStatusMessage) {
            lab->setText(m_myStatusMessage);
        }
        return;
    }

    lab->setText(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
    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
}

void
MainWindowBase::openLocalFolder(QString path)
{
    QDir d(path);
    if (d.exists()) {
        QStringList args;
        QString path = d.canonicalPath();
#if defined Q_OS_WIN32
        // Although the Win32 API is quite happy to have
        // forward slashes as directory separators, Windows
        // Explorer is not
        path = path.replace('/', '\\');
        args << path;
        QProcess::execute("c:/windows/explorer.exe", args);
#else
        args << path;
        QProcess::execute(
#if defined Q_OS_MAC
            "/usr/bin/open",
#else
            "/usr/bin/xdg-open",
#endif
            args);
#endif
    }
}