view src/MainWindow.cpp @ 207:a3e9ddb7bb8b

OS/X deployment: include plugins in bundle, use qt.conf instead of programmatic means of finding them. Now that the accessibility plugin is found, the toolbars are being rendered correctly
author Chris Cannam
date Thu, 06 Mar 2014 12:42:25 +0000
parents 40e6ba379f2c
children e79f63161b41
line wrap: on
line source
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */

/*
    Tony
    An intonation analysis and annotation tool
    Centre for Digital Music, Queen Mary, University of London.
    This file copyright 2006-2012 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 "../version.h"

#include "MainWindow.h"
#include "NetworkPermissionTester.h"
#include "Analyser.h"

#include "framework/Document.h"
#include "framework/VersionTester.h"

#include "view/Pane.h"
#include "view/PaneStack.h"
#include "data/model/WaveFileModel.h"
#include "data/model/NoteModel.h"
#include "data/model/FlexiNoteModel.h"
#include "layer/FlexiNoteLayer.h"
#include "data/model/NoteModel.h"
#include "view/ViewManager.h"
#include "base/Preferences.h"
#include "layer/WaveformLayer.h"
#include "layer/TimeInstantLayer.h"
#include "layer/TimeValueLayer.h"
#include "layer/SpectrogramLayer.h"
#include "widgets/Fader.h"
#include "view/Overview.h"
#include "widgets/AudioDial.h"
#include "widgets/IconLoader.h"
#include "widgets/KeyReference.h"
#include "audioio/AudioCallbackPlaySource.h"
#include "audioio/AudioCallbackPlayTarget.h"
#include "audioio/PlaySpeedRangeMapper.h"
#include "base/Profiler.h"
#include "base/UnitDatabase.h"
#include "layer/ColourDatabase.h"
#include "base/Selection.h"

#include "rdf/RDFImporter.h"
#include "data/fileio/DataFileReaderFactory.h"
#include "data/fileio/CSVFormat.h"
#include "data/fileio/CSVFileWriter.h"
#include "data/fileio/MIDIFileWriter.h"
#include "rdf/RDFExporter.h"

// For version information
#include "vamp/vamp.h"
#include "vamp-sdk/PluginBase.h"
#include "plugin/api/ladspa.h"
#include "plugin/api/dssi.h"

#include <QApplication>
#include <QMessageBox>
#include <QGridLayout>
#include <QLabel>
#include <QMenuBar>
#include <QToolBar>
#include <QToolButton>
#include <QInputDialog>
#include <QStatusBar>
#include <QFileInfo>
#include <QDir>
#include <QProcess>
#include <QPushButton>
#include <QSettings>
#include <QScrollArea>
#include <QPainter>

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

using std::vector;


MainWindow::MainWindow(bool withAudioOutput, bool withOSCSupport) :
    MainWindowBase(withAudioOutput, withOSCSupport, false),
    m_overview(0),
    m_mainMenusCreated(false),
    m_playbackMenu(0),
    m_recentFilesMenu(0), 
    m_rightButtonMenu(0),
    m_rightButtonPlaybackMenu(0),
    m_deleteSelectedAction(0),
    m_ffwdAction(0),
    m_rwdAction(0),
    m_intelligentActionOn(true), //GF: !!! temporary
    m_keyReference(new KeyReference())
{
    setWindowTitle(QApplication::applicationName());

#ifdef Q_OS_MAC
#if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0))
    setUnifiedTitleAndToolBarOnMac(true);
#endif
#endif

    UnitDatabase *udb = UnitDatabase::getInstance();
    udb->registerUnit("Hz");
    udb->registerUnit("dB");
    udb->registerUnit("s");

    ColourDatabase *cdb = ColourDatabase::getInstance();
    cdb->addColour(Qt::black, tr("Black"));
    cdb->addColour(Qt::darkRed, tr("Red"));
    cdb->addColour(Qt::darkBlue, tr("Blue"));
    cdb->addColour(Qt::darkGreen, tr("Green"));
    cdb->addColour(QColor(200, 50, 255), tr("Purple"));
    cdb->addColour(QColor(255, 150, 50), tr("Orange"));
    cdb->addColour(QColor(180, 180, 180), tr("Grey"));
    cdb->setUseDarkBackground(cdb->addColour(Qt::white, tr("White")), true);
    cdb->setUseDarkBackground(cdb->addColour(Qt::red, tr("Bright Red")), true);
    cdb->setUseDarkBackground(cdb->addColour(QColor(30, 150, 255), tr("Bright Blue")), true);
    cdb->setUseDarkBackground(cdb->addColour(Qt::green, tr("Bright Green")), true);
    cdb->setUseDarkBackground(cdb->addColour(QColor(225, 74, 255), tr("Bright Purple")), true);
    cdb->setUseDarkBackground(cdb->addColour(QColor(255, 188, 80), tr("Bright Orange")), true);

    Preferences::getInstance()->setResampleOnLoad(true);
    Preferences::getInstance()->setSpectrogramSmoothing
        (Preferences::SpectrogramInterpolated);

    QSettings settings;

    settings.beginGroup("MainWindow");
    settings.setValue("showstatusbar", false);
    settings.endGroup();

    settings.beginGroup("Transformer");
    settings.setValue("use-flexi-note-model", true);
    settings.endGroup();

    settings.beginGroup("LayerDefaults");
    settings.setValue("waveform",
                      QString("<layer scale=\"%1\" channelMode=\"%2\"/>")
                      .arg(int(WaveformLayer::LinearScale))
                      .arg(int(WaveformLayer::MixChannels)));
    settings.endGroup();

    m_viewManager->setAlignMode(false);
    m_viewManager->setPlaySoloMode(false);
    m_viewManager->setToolMode(ViewManager::NavigateMode);
    m_viewManager->setZoomWheelsEnabled(false);
    m_viewManager->setIlluminateLocalFeatures(true);
    m_viewManager->setShowWorkTitle(true);
    m_viewManager->setShowCentreLine(false);
    m_viewManager->setOverlayMode(ViewManager::MinimalOverlays);

    connect(m_viewManager, SIGNAL(selectionChangedByUser()),
	    this, SLOT(selectionChangedByUser()));

    QFrame *frame = new QFrame;
    setCentralWidget(frame);

    QGridLayout *layout = new QGridLayout;
    
    QScrollArea *scroll = new QScrollArea(frame);
    scroll->setWidgetResizable(true);
    scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    scroll->setFrameShape(QFrame::NoFrame);

    // We have a pane stack: it comes with the territory. However, we
    // have a fixed and known number of panes in it -- it isn't
    // variable
    m_paneStack->setLayoutStyle(PaneStack::NoPropertyStacks);
    m_paneStack->setShowPaneAccessories(false);
    connect(m_paneStack, SIGNAL(doubleClickSelectInvoked(size_t)),
            this, SLOT(doubleClickSelectInvoked(size_t)));
    scroll->setWidget(m_paneStack);

    m_overview = new Overview(frame);
    m_overview->setViewManager(m_viewManager);
    m_overview->setFixedHeight(40);
#ifndef _WIN32
    // For some reason, the contents of the overview never appear if we
    // make this setting on Windows.  I have no inclination at the moment
    // to track down the reason why.
    m_overview->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
#endif
    connect(m_overview, SIGNAL(contextHelpChanged(const QString &)),
            this, SLOT(contextHelpChanged(const QString &)));

    m_panLayer = new WaveformLayer;
    m_panLayer->setChannelMode(WaveformLayer::MergeChannels);
    m_panLayer->setAggressiveCacheing(true);
    m_overview->addLayer(m_panLayer);

    if (m_viewManager->getGlobalDarkBackground()) {
        m_panLayer->setBaseColour
            (ColourDatabase::getInstance()->getColourIndex(tr("Bright Green")));
    } else {
        m_panLayer->setBaseColour
            (ColourDatabase::getInstance()->getColourIndex(tr("Blue")));
    }        

    m_fader = new Fader(frame, false);
    connect(m_fader, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_fader, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

    m_playSpeed = new AudioDial(frame);
    m_playSpeed->setMinimum(0);
    m_playSpeed->setMaximum(200);
    m_playSpeed->setValue(100);
    m_playSpeed->setFixedWidth(24);
    m_playSpeed->setFixedHeight(24);
    m_playSpeed->setNotchesVisible(true);
    m_playSpeed->setPageStep(10);
    m_playSpeed->setObjectName(tr("Playback Speedup"));
    m_playSpeed->setDefaultValue(100);
    m_playSpeed->setRangeMapper(new PlaySpeedRangeMapper(0, 200));
    m_playSpeed->setShowToolTip(true);
    connect(m_playSpeed, SIGNAL(valueChanged(int)),
        this, SLOT(playSpeedChanged(int)));
    connect(m_playSpeed, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_playSpeed, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

    // Gain controls
    m_gainAudio = new AudioDial(frame);
    m_gainAudio->setMinimum(-50);
    m_gainAudio->setMaximum(50);
    m_gainAudio->setValue(0);
    m_gainAudio->setDefaultValue(0);
    m_gainAudio->setFixedWidth(24);
    m_gainAudio->setFixedHeight(24);
    m_gainAudio->setNotchesVisible(true);
    m_gainAudio->setPageStep(10);
    m_gainAudio->setObjectName(tr("Audio Track Gain"));
    m_gainAudio->setRangeMapper(new LinearRangeMapper(-50, 50, -25, 25, tr("dB")));
    m_gainAudio->setShowToolTip(true);
    connect(m_gainAudio, SIGNAL(valueChanged(int)),
            this, SLOT(audioGainChanged(int)));
    connect(m_gainAudio, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_gainAudio, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

    m_gainPitch = new AudioDial(frame);
    m_gainPitch->setMinimum(-50);
    m_gainPitch->setMaximum(50);
    m_gainPitch->setValue(0);
    m_gainPitch->setDefaultValue(0);
    m_gainPitch->setFixedWidth(24);
    m_gainPitch->setFixedHeight(24);
    m_gainPitch->setNotchesVisible(true);
    m_gainPitch->setPageStep(10);
    m_gainPitch->setObjectName(tr("Pitch Track Gain"));
    m_gainPitch->setRangeMapper(new LinearRangeMapper(-50, 50, -25, 25, tr("dB")));
    m_gainPitch->setShowToolTip(true);
    connect(m_gainPitch, SIGNAL(valueChanged(int)),
            this, SLOT(pitchGainChanged(int)));
    connect(m_gainPitch, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_gainPitch, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

    m_gainNotes = new AudioDial(frame);
    m_gainNotes->setMinimum(-50);
    m_gainNotes->setMaximum(50);
    m_gainNotes->setValue(0);
    m_gainNotes->setDefaultValue(0);
    m_gainNotes->setFixedWidth(24);
    m_gainNotes->setFixedHeight(24);
    m_gainNotes->setNotchesVisible(true);
    m_gainNotes->setPageStep(10);
    m_gainNotes->setObjectName(tr("Pitch Track Gain"));
    m_gainNotes->setRangeMapper(new LinearRangeMapper(-50, 50, -25, 25, tr("dB")));
    m_gainNotes->setShowToolTip(true);
    connect(m_gainNotes, SIGNAL(valueChanged(int)),
            this, SLOT(notesGainChanged(int)));
    connect(m_gainNotes, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_gainNotes, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));
    // End of Gain controls

    // Pan controls
    m_panAudio = new AudioDial(frame);
    m_panAudio->setMinimum(-100);
    m_panAudio->setMaximum(100);
    m_panAudio->setValue(-100);
    m_panAudio->setDefaultValue(-100);
    m_panAudio->setFixedWidth(24);
    m_panAudio->setFixedHeight(24);
    m_panAudio->setNotchesVisible(true);
    m_panAudio->setPageStep(10);
    m_panAudio->setObjectName(tr("Audio Track Pan"));
    m_panAudio->setRangeMapper(new LinearRangeMapper(-100, 100, -100, 100, tr("")));
    m_panAudio->setShowToolTip(true);
    connect(m_panAudio, SIGNAL(valueChanged(int)),
            this, SLOT(audioPanChanged(int)));
    connect(m_panAudio, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_panAudio, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

    m_panPitch = new AudioDial(frame);
    m_panPitch->setMinimum(-100);
    m_panPitch->setMaximum(100);
    m_panPitch->setValue(100);
    m_panPitch->setDefaultValue(100);
    m_panPitch->setFixedWidth(24);
    m_panPitch->setFixedHeight(24);
    m_panPitch->setNotchesVisible(true);
    m_panPitch->setPageStep(10);
    m_panPitch->setObjectName(tr("Pitch Track Pan"));
    m_panPitch->setRangeMapper(new LinearRangeMapper(-100, 100, -100, 100, tr("")));
    m_panPitch->setShowToolTip(true);
    connect(m_panPitch, SIGNAL(valueChanged(int)),
            this, SLOT(pitchPanChanged(int)));
    connect(m_panPitch, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_panPitch, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

    m_panNotes = new AudioDial(frame);
    m_panNotes->setMinimum(-100);
    m_panNotes->setMaximum(100);
    m_panNotes->setValue(100);
    m_panNotes->setDefaultValue(100);
    m_panNotes->setFixedWidth(24);
    m_panNotes->setFixedHeight(24);
    m_panNotes->setNotchesVisible(true);
    m_panNotes->setPageStep(10);
    m_panNotes->setObjectName(tr("Notes Track Pan"));
    m_panNotes->setRangeMapper(new LinearRangeMapper(-100, 100, -100, 100, tr("")));
    m_panNotes->setShowToolTip(true);
    connect(m_panNotes, SIGNAL(valueChanged(int)),
            this, SLOT(notesPanChanged(int)));
    connect(m_panNotes, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
    connect(m_panNotes, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));
    // End of Pan controls

    layout->setSpacing(4);
    layout->addWidget(m_overview, 0, 1);
    layout->addWidget(scroll, 1, 1);

    layout->setColumnStretch(1, 10);

    frame->setLayout(layout);

    m_analyser = new Analyser();
    connect(m_analyser, SIGNAL(layersChanged()),
            this, SLOT(updateLayerStatuses()));
    connect(m_analyser, SIGNAL(layersChanged()),
            this, SLOT(updateMenuStates()));

    setupMenus();
    setupToolbars();
    setupHelpMenu();

    statusBar();

    newSession();

    settings.beginGroup("MainWindow");
    settings.setValue("zoom-default", 512);
    settings.endGroup();
    zoomDefault();

    NetworkPermissionTester tester;
    bool networkPermission = tester.havePermission();
    if (networkPermission) {
        m_versionTester = new VersionTester
            ("sonicvisualiser.org", "latest-tony-version.txt", TONY_VERSION);
        connect(m_versionTester, SIGNAL(newerVersionAvailable(QString)),
                this, SLOT(newerVersionAvailable(QString)));
    } else {
        m_versionTester = 0;
    }
}

MainWindow::~MainWindow()
{
    delete m_analyser;
    delete m_keyReference;
    Profiles::getInstance()->dump();
}

void
MainWindow::setupMenus()
{
    if (!m_mainMenusCreated) {
        m_rightButtonMenu = new QMenu();
    }

    if (!m_mainMenusCreated) {
        CommandHistory::getInstance()->registerMenu(m_rightButtonMenu);
        m_rightButtonMenu->addSeparator();
    }

    setupFileMenu();
    setupEditMenu();
    setupViewMenu();

    m_mainMenusCreated = true;
}

void
MainWindow::setupFileMenu()
{
    if (m_mainMenusCreated) return;

    QMenu *menu = menuBar()->addMenu(tr("&File"));
    menu->setTearOffEnabled(true);
    QToolBar *toolbar = addToolBar(tr("File Toolbar"));

    m_keyReference->setCategory(tr("File and Session Management"));

    IconLoader il;
    QIcon icon;
    QAction *action;

    icon = il.load("fileopen");
    icon.addPixmap(il.loadPixmap("fileopen-22"));
    action = new QAction(icon, tr("&Open..."), this);
    action->setShortcut(tr("Ctrl+O"));
    action->setStatusTip(tr("Open a file"));
    connect(action, SIGNAL(triggered()), this, SLOT(openFile()));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    toolbar->addAction(action);

    action = new QAction(tr("Open Lo&cation..."), this);
    action->setShortcut(tr("Ctrl+Shift+O"));
    action->setStatusTip(tr("Open a file from a remote URL"));
    connect(action, SIGNAL(triggered()), this, SLOT(openLocation()));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);

    m_recentFilesMenu = menu->addMenu(tr("Open &Recent"));
    m_recentFilesMenu->setTearOffEnabled(true);
    setupRecentFilesMenu();
    connect(&m_recentFiles, SIGNAL(recentChanged()),
            this, SLOT(setupRecentFilesMenu()));

    menu->addSeparator();
    action = new QAction(tr("Import Pitch Track Data..."), this);
    action->setStatusTip(tr("Import pitch-track data from a file"));
    connect(action, SIGNAL(triggered()), this, SLOT(importPitchLayer()));
    connect(this, SIGNAL(canImportLayer(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);

    action = new QAction(tr("Export Pitch Track Data..."), this);
    action->setStatusTip(tr("Export pitch-track data to a file"));
    connect(action, SIGNAL(triggered()), this, SLOT(exportPitchLayer()));
    connect(this, SIGNAL(canExportLayer(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);

    action = new QAction(tr("Export Note Data..."), this);
    action->setStatusTip(tr("Export note data to a file"));
    connect(action, SIGNAL(triggered()), this, SLOT(exportNoteLayer()));
    connect(this, SIGNAL(canExportLayer(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);

    menu->addSeparator();
    action = new QAction(il.load("exit"), tr("&Quit"), this);
    action->setShortcut(tr("Ctrl+Q"));
    action->setStatusTip(tr("Exit %1").arg(QApplication::applicationName()));
    connect(action, SIGNAL(triggered()), this, SLOT(close()));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
}

void
MainWindow::setupEditMenu()
{
    if (m_mainMenusCreated) return;

    QMenu *menu = menuBar()->addMenu(tr("&Edit"));
    menu->setTearOffEnabled(true);
    CommandHistory::getInstance()->registerMenu(menu);
    menu->addSeparator();

    QToolBar *toolbar = addToolBar(tr("Tools Toolbar"));
    
    CommandHistory::getInstance()->registerToolbar(toolbar);

    m_keyReference->setCategory(tr("Tool Selection"));

    QActionGroup *group = new QActionGroup(this);

    IconLoader il;

    QAction *action = toolbar->addAction(il.load("navigate"),
                                         tr("Navigate"));
    action->setCheckable(true);
    action->setChecked(true);
    action->setShortcut(tr("1"));
    action->setStatusTip(tr("Navigate"));
    connect(action, SIGNAL(triggered()), this, SLOT(toolNavigateSelected()));
    connect(this, SIGNAL(replacedDocument()), action, SLOT(trigger()));
    group->addAction(action);
    menu->addAction(action);
    m_keyReference->registerShortcut(action);

    action = toolbar->addAction(il.load("move"),
				tr("Edit"));
    action->setCheckable(true);
    action->setShortcut(tr("2"));
    action->setStatusTip(tr("Edit with Note Intelligence"));
    connect(action, SIGNAL(triggered()), this, SLOT(toolEditSelected()));
    connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool)));
    group->addAction(action);
    menu->addAction(action);
    m_keyReference->registerShortcut(action);

/* Remove for now...

    action = toolbar->addAction(il.load("notes"),
				tr("Free Edit"));
    action->setCheckable(true);
    action->setShortcut(tr("3"));
    action->setStatusTip(tr("Free Edit"));
    connect(action, SIGNAL(triggered()), this, SLOT(toolFreeEditSelected()));
    connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool)));
    group->addAction(action);
    m_keyReference->registerShortcut(action);
*/

    menu->addSeparator();
    
    m_keyReference->setCategory(tr("Selection"));

    action = new QAction(tr("Select &All"), this);
    action->setShortcut(tr("Ctrl+A"));
    action->setStatusTip(tr("Select the whole duration of the current session"));
    connect(action, SIGNAL(triggered()), this, SLOT(selectAll()));
    connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    m_rightButtonMenu->addAction(action);

    action = new QAction(tr("C&lear Selection"), this);
    action->setShortcut(tr("Esc"));
    action->setStatusTip(tr("Clear the selection"));
    connect(action, SIGNAL(triggered()), this, SLOT(abandonSelection()));
    connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    m_rightButtonMenu->addAction(action);

    menu->addSeparator();
    m_rightButtonMenu->addSeparator();
    
    m_keyReference->setCategory(tr("Pitch Track"));

    m_showCandidatesAction = new QAction(tr("Show Pitch Candidates"), this);
    m_showCandidatesAction->setShortcut(tr("Ctrl+Return"));
    m_showCandidatesAction->setStatusTip(tr("Toggle the display of alternative pitch candidates for the selected region"));
    m_keyReference->registerShortcut(m_showCandidatesAction);
    connect(m_showCandidatesAction, SIGNAL(triggered()), this, SLOT(togglePitchCandidates()));
    connect(this, SIGNAL(canClearSelection(bool)), m_showCandidatesAction, SLOT(setEnabled(bool)));
    menu->addAction(m_showCandidatesAction);
    m_rightButtonMenu->addAction(m_showCandidatesAction);
    
    action = new QAction(tr("Pick Higher Pitch Candidate"), this);
    action->setShortcut(tr("Ctrl+Up"));
    action->setStatusTip(tr("Switch to the next higher pitch candidate in the selected region"));
    m_keyReference->registerShortcut(action);
    connect(action, SIGNAL(triggered()), this, SLOT(switchPitchUp()));
    connect(this, SIGNAL(canChangeToHigherCandidate(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);
    m_rightButtonMenu->addAction(action);
    
    action = new QAction(tr("Pick Lower Pitch Candidate"), this);
    action->setShortcut(tr("Ctrl+Down"));
    action->setStatusTip(tr("Switch to the next lower pitch candidate in the selected region"));
    m_keyReference->registerShortcut(action);
    connect(action, SIGNAL(triggered()), this, SLOT(switchPitchDown()));
    connect(this, SIGNAL(canChangeToLowerCandidate(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);
    m_rightButtonMenu->addAction(action);
    
    menu->addSeparator();
    m_rightButtonMenu->addSeparator();
    
    action = new QAction(tr("Octave Shift Up"), this);
    action->setShortcut(tr("PgUp"));
    action->setStatusTip(tr("Move all pitches up an octave in the selected region"));    
    m_keyReference->registerShortcut(action);
    connect(action, SIGNAL(triggered()), this, SLOT(octaveShiftUp()));
    connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);
    m_rightButtonMenu->addAction(action);

    action = new QAction(tr("Octave Shift Down"), this);
    action->setShortcut(tr("PgDown"));
    action->setStatusTip(tr("Move all pitches down an octave in the selected region"));    
    m_keyReference->registerShortcut(action);
    connect(action, SIGNAL(triggered()), this, SLOT(octaveShiftDown()));
    connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);
    m_rightButtonMenu->addAction(action);

    menu->addSeparator();
    m_rightButtonMenu->addSeparator();
    
    action = new QAction(tr("Remove Pitches"), this);
    action->setShortcut(tr("Ctrl+Backspace"));
    action->setStatusTip(tr("Remove all pitch estimates within the selected region (converting it to unvoiced)"));
    m_keyReference->registerShortcut(action);
    connect(action, SIGNAL(triggered()), this, SLOT(clearPitches()));
    connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);
    m_rightButtonMenu->addAction(action);
}

void
MainWindow::setupViewMenu()
{
    if (m_mainMenusCreated) return;

    IconLoader il;

    QAction *action = 0;

    m_keyReference->setCategory(tr("Panning and Navigation"));

    QMenu *menu = menuBar()->addMenu(tr("&View"));
    menu->setTearOffEnabled(true);
    action = new QAction(tr("Scroll &Left"), this);
    action->setShortcut(tr("Left"));
    action->setStatusTip(tr("Scroll the current pane to the left"));
    connect(action, SIGNAL(triggered()), this, SLOT(scrollLeft()));
    connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    
    action = new QAction(tr("Scroll &Right"), this);
    action->setShortcut(tr("Right"));
    action->setStatusTip(tr("Scroll the current pane to the right"));
    connect(action, SIGNAL(triggered()), this, SLOT(scrollRight()));
    connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    
    action = new QAction(tr("&Jump Left"), this);
    action->setShortcut(tr("Ctrl+Left"));
    action->setStatusTip(tr("Scroll the current pane a big step to the left"));
    connect(action, SIGNAL(triggered()), this, SLOT(jumpLeft()));
    connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    
    action = new QAction(tr("J&ump Right"), this);
    action->setShortcut(tr("Ctrl+Right"));
    action->setStatusTip(tr("Scroll the current pane a big step to the right"));
    connect(action, SIGNAL(triggered()), this, SLOT(jumpRight()));
    connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);

    menu->addSeparator();

    m_keyReference->setCategory(tr("Zoom"));

    action = new QAction(il.load("zoom-in"),
                         tr("Zoom &In"), this);
    action->setShortcut(tr("Up"));
    action->setStatusTip(tr("Increase the zoom level"));
    connect(action, SIGNAL(triggered()), this, SLOT(zoomIn()));
    connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    
    action = new QAction(il.load("zoom-out"),
                         tr("Zoom &Out"), this);
    action->setShortcut(tr("Down"));
    action->setStatusTip(tr("Decrease the zoom level"));
    connect(action, SIGNAL(triggered()), this, SLOT(zoomOut()));
    connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    
    action = new QAction(tr("Restore &Default Zoom"), this);
    action->setStatusTip(tr("Restore the zoom level to the default"));
    connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault()));
    connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
    menu->addAction(action);

    action = new QAction(il.load("zoom-fit"),
                         tr("Zoom to &Fit"), this);
    action->setShortcut(tr("F"));
    action->setStatusTip(tr("Zoom to show the whole file"));
    connect(action, SIGNAL(triggered()), this, SLOT(zoomToFit()));
    connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
}

void
MainWindow::setupHelpMenu()
{
    QMenu *menu = menuBar()->addMenu(tr("&Help"));
    menu->setTearOffEnabled(true);
    
    m_keyReference->setCategory(tr("Help"));

    IconLoader il;

    QString name = QApplication::applicationName();

    QAction *action = new QAction(il.load("help"),
                                  tr("&Help Reference"), this); 
    action->setShortcut(tr("F1"));
    action->setStatusTip(tr("Open the %1 reference manual").arg(name)); 
    connect(action, SIGNAL(triggered()), this, SLOT(help()));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);

    action = new QAction(tr("&Key and Mouse Reference"), this);
    action->setShortcut(tr("F2"));
    action->setStatusTip(tr("Open a window showing the keystrokes you can use in %1").arg(name));
    connect(action, SIGNAL(triggered()), this, SLOT(keyReference()));
    m_keyReference->registerShortcut(action);
    menu->addAction(action);
    
    action = new QAction(tr("%1 on the &Web").arg(name), this); 
    action->setStatusTip(tr("Open the %1 website").arg(name)); 
    connect(action, SIGNAL(triggered()), this, SLOT(website()));
    menu->addAction(action);
    
    action = new QAction(tr("&About %1").arg(name), this); 
    action->setStatusTip(tr("Show information about %1").arg(name)); 
    connect(action, SIGNAL(triggered()), this, SLOT(about()));
    menu->addAction(action);
}

void
MainWindow::setupRecentFilesMenu()
{
    m_recentFilesMenu->clear();
    vector<QString> files = m_recentFiles.getRecent();
    for (size_t i = 0; i < files.size(); ++i) {
        QAction *action = new QAction(files[i], this);
        connect(action, SIGNAL(triggered()), this, SLOT(openRecentFile()));
        if (i == 0) {
            action->setShortcut(tr("Ctrl+R"));
            m_keyReference->registerShortcut
                (tr("Re-open"),
                 action->shortcut().toString(),
                 tr("Re-open the current or most recently opened file"));
        }
        m_recentFilesMenu->addAction(action);
    }
}

void
MainWindow::setupToolbars()
{
    m_keyReference->setCategory(tr("Playback and Transport Controls"));

    IconLoader il;

    QMenu *menu = m_playbackMenu = menuBar()->addMenu(tr("Play&back"));
    menu->setTearOffEnabled(true);
    m_rightButtonMenu->addSeparator();
    m_rightButtonPlaybackMenu = m_rightButtonMenu->addMenu(tr("Playback"));

    QToolBar *toolbar = addToolBar(tr("Playback Toolbar"));

    QAction *rwdStartAction = toolbar->addAction(il.load("rewind-start"),
                                                 tr("Rewind to Start"));
    rwdStartAction->setShortcut(tr("Home"));
    rwdStartAction->setStatusTip(tr("Rewind to the start"));
    connect(rwdStartAction, SIGNAL(triggered()), this, SLOT(rewindStart()));
    connect(this, SIGNAL(canPlay(bool)), rwdStartAction, SLOT(setEnabled(bool)));

    QAction *m_rwdAction = toolbar->addAction(il.load("rewind"),
                                              tr("Rewind"));
    m_rwdAction->setStatusTip(tr("Rewind to the previous time instant or time ruler notch"));
    connect(m_rwdAction, SIGNAL(triggered()), this, SLOT(rewind()));
    connect(this, SIGNAL(canRewind(bool)), m_rwdAction, SLOT(setEnabled(bool)));

    QAction *playAction = toolbar->addAction(il.load("playpause"),
                                             tr("Play / Pause"));
    playAction->setCheckable(true);
    playAction->setShortcut(tr("Space"));
    playAction->setStatusTip(tr("Start or stop playback from the current position"));
    connect(playAction, SIGNAL(triggered()), this, SLOT(play()));
    connect(m_playSource, SIGNAL(playStatusChanged(bool)),
        playAction, SLOT(setChecked(bool)));
    connect(this, SIGNAL(canPlay(bool)), playAction, SLOT(setEnabled(bool)));

    m_ffwdAction = toolbar->addAction(il.load("ffwd"),
                                              tr("Fast Forward"));
    m_ffwdAction->setStatusTip(tr("Fast-forward to the next time instant or time ruler notch"));
    connect(m_ffwdAction, SIGNAL(triggered()), this, SLOT(ffwd()));
    connect(this, SIGNAL(canFfwd(bool)), m_ffwdAction, SLOT(setEnabled(bool)));

    QAction *ffwdEndAction = toolbar->addAction(il.load("ffwd-end"),
                                                tr("Fast Forward to End"));
    ffwdEndAction->setShortcut(tr("End"));
    ffwdEndAction->setStatusTip(tr("Fast-forward to the end"));
    connect(ffwdEndAction, SIGNAL(triggered()), this, SLOT(ffwdEnd()));
    connect(this, SIGNAL(canPlay(bool)), ffwdEndAction, SLOT(setEnabled(bool)));

    toolbar = addToolBar(tr("Play Mode Toolbar"));

    QAction *psAction = toolbar->addAction(il.load("playselection"),
                                           tr("Constrain Playback to Selection"));
    psAction->setCheckable(true);
    psAction->setChecked(m_viewManager->getPlaySelectionMode());
    psAction->setShortcut(tr("s"));
    psAction->setStatusTip(tr("Constrain playback to the selected regions"));
    connect(m_viewManager, SIGNAL(playSelectionModeChanged(bool)),
            psAction, SLOT(setChecked(bool)));
    connect(psAction, SIGNAL(triggered()), this, SLOT(playSelectionToggled()));
    connect(this, SIGNAL(canPlaySelection(bool)), psAction, SLOT(setEnabled(bool)));

    QAction *plAction = toolbar->addAction(il.load("playloop"),
                                           tr("Loop Playback"));
    plAction->setCheckable(true);
    plAction->setChecked(m_viewManager->getPlayLoopMode());
    plAction->setShortcut(tr("l"));
    plAction->setStatusTip(tr("Loop playback"));
    connect(m_viewManager, SIGNAL(playLoopModeChanged(bool)),
            plAction, SLOT(setChecked(bool)));
    connect(plAction, SIGNAL(triggered()), this, SLOT(playLoopToggled()));
    connect(this, SIGNAL(canPlay(bool)), plAction, SLOT(setEnabled(bool)));

    m_keyReference->registerShortcut(psAction);
    m_keyReference->registerShortcut(plAction);
    m_keyReference->registerShortcut(playAction);
    m_keyReference->registerShortcut(m_rwdAction);
    m_keyReference->registerShortcut(m_ffwdAction);
    m_keyReference->registerShortcut(rwdStartAction);
    m_keyReference->registerShortcut(ffwdEndAction);

    menu->addAction(playAction);
    menu->addAction(psAction);
    menu->addAction(plAction);
    menu->addSeparator();
    menu->addAction(m_rwdAction);
    menu->addAction(m_ffwdAction);
    menu->addSeparator();
    menu->addAction(rwdStartAction);
    menu->addAction(ffwdEndAction);
    menu->addSeparator();

    m_rightButtonPlaybackMenu->addAction(playAction);
    m_rightButtonPlaybackMenu->addAction(psAction);
    m_rightButtonPlaybackMenu->addAction(plAction);
    m_rightButtonPlaybackMenu->addSeparator();
    m_rightButtonPlaybackMenu->addAction(m_rwdAction);
    m_rightButtonPlaybackMenu->addAction(m_ffwdAction);
    m_rightButtonPlaybackMenu->addSeparator();
    m_rightButtonPlaybackMenu->addAction(rwdStartAction);
    m_rightButtonPlaybackMenu->addAction(ffwdEndAction);
    m_rightButtonPlaybackMenu->addSeparator();

    QAction *fastAction = menu->addAction(tr("Speed Up"));
    fastAction->setShortcut(tr("Ctrl+PgUp"));
    fastAction->setStatusTip(tr("Time-stretch playback to speed it up without changing pitch"));
    connect(fastAction, SIGNAL(triggered()), this, SLOT(speedUpPlayback()));
    connect(this, SIGNAL(canSpeedUpPlayback(bool)), fastAction, SLOT(setEnabled(bool)));
    
    QAction *slowAction = menu->addAction(tr("Slow Down"));
    slowAction->setShortcut(tr("Ctrl+PgDown"));
    slowAction->setStatusTip(tr("Time-stretch playback to slow it down without changing pitch"));
    connect(slowAction, SIGNAL(triggered()), this, SLOT(slowDownPlayback()));
    connect(this, SIGNAL(canSlowDownPlayback(bool)), slowAction, SLOT(setEnabled(bool)));

    QAction *normalAction = menu->addAction(tr("Restore Normal Speed"));
    normalAction->setShortcut(tr("Ctrl+Home"));
    normalAction->setStatusTip(tr("Restore non-time-stretched playback"));
    connect(normalAction, SIGNAL(triggered()), this, SLOT(restoreNormalPlayback()));
    connect(this, SIGNAL(canChangePlaybackSpeed(bool)), normalAction, SLOT(setEnabled(bool)));

    m_keyReference->registerShortcut(fastAction);
    m_keyReference->registerShortcut(slowAction);
    m_keyReference->registerShortcut(normalAction);

    m_rightButtonPlaybackMenu->addAction(fastAction);
    m_rightButtonPlaybackMenu->addAction(slowAction);
    m_rightButtonPlaybackMenu->addAction(normalAction);

    toolbar = new QToolBar(tr("Playback Controls"));
    addToolBar(Qt::BottomToolBarArea, toolbar);

    toolbar->addWidget(m_playSpeed);
    toolbar->addWidget(m_fader);

    toolbar = addToolBar(tr("Show and Play"));

    /* ORIGINAL DESIGN
    QLabel *eye = new QLabel;
    eye->setFixedWidth(40);
    eye->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    eye->setPixmap(il.loadPixmap("eye"));
    toolbar->addWidget(eye);

    m_showAudio = toolbar->addAction(il.load("waveform"), tr("Show Audio"));
    m_showAudio->setCheckable(true);
    connect(m_showAudio, SIGNAL(triggered()), this, SLOT(showAudioToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showAudio, SLOT(setEnabled(bool)));

    m_showSpect = toolbar->addAction(il.load("spectrogram"), tr("Show Spectrogram"));
    m_showSpect->setCheckable(true);
    connect(m_showSpect, SIGNAL(triggered()), this, SLOT(showSpectToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showSpect, SLOT(setEnabled(bool)));

    m_showPitch = toolbar->addAction(il.load("values"), tr("Show Pitch Track"));
    m_showPitch->setCheckable(true);
    connect(m_showPitch, SIGNAL(triggered()), this, SLOT(showPitchToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showPitch, SLOT(setEnabled(bool)));

    m_showNotes = toolbar->addAction(il.load("notes"), tr("Show Notes"));
    m_showNotes->setCheckable(true);
    connect(m_showNotes, SIGNAL(triggered()), this, SLOT(showNotesToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showNotes, SLOT(setEnabled(bool)));
        
    QLabel *speaker = new QLabel;
    speaker->setFixedWidth(40);
    speaker->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    speaker->setPixmap(il.loadPixmap("speaker"));
    toolbar->addWidget(speaker);
    
    m_playAudio = toolbar->addAction(il.load("waveform"), tr("Play Audio"));
    m_playAudio->setCheckable(true);
    connect(m_playAudio, SIGNAL(triggered()), this, SLOT(playAudioToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_playAudio, SLOT(setEnabled(bool)));

    m_playPitch = toolbar->addAction(il.load("values"), tr("Play Pitch Track"));
    m_playPitch->setCheckable(true);
    connect(m_playPitch, SIGNAL(triggered()), this, SLOT(playPitchToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_playPitch, SLOT(setEnabled(bool)));

    m_playNotes = toolbar->addAction(il.load("notes"), tr("Play Notes"));
    m_playNotes->setCheckable(true);
    connect(m_playNotes, SIGNAL(triggered()), this, SLOT(playNotesToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_playNotes, SLOT(setEnabled(bool)));
    */

    // Audio
    QLabel *icon_audio = new QLabel;
    icon_audio->setFixedWidth(40);
    icon_audio->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    icon_audio->setPixmap(il.loadPixmap("waveform"));
    toolbar->addWidget(icon_audio);

    m_showAudio = toolbar->addAction(il.load("eye"), tr("Show Audio"));
    m_showAudio->setCheckable(true);
    connect(m_showAudio, SIGNAL(triggered()), this, SLOT(showAudioToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showAudio, SLOT(setEnabled(bool)));

    m_playAudio = toolbar->addAction(il.load("speaker"), tr("Play Audio"));
    m_playAudio->setCheckable(true);
    connect(m_playAudio, SIGNAL(triggered()), this, SLOT(playAudioToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_playAudio, SLOT(setEnabled(bool)));

    toolbar->addWidget(m_gainAudio);
    toolbar->addWidget(m_panAudio);

    // Pitch (f0)
    QLabel *icon_pitch = new QLabel;
    icon_pitch->setFixedWidth(40);
    icon_pitch->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    icon_pitch->setPixmap(il.loadPixmap("values"));
    toolbar->addWidget(icon_pitch);

    m_showPitch = toolbar->addAction(il.load("eye"), tr("Show Pitch Track"));
    m_showPitch->setCheckable(true);
    connect(m_showPitch, SIGNAL(triggered()), this, SLOT(showPitchToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showPitch, SLOT(setEnabled(bool)));

    m_playPitch = toolbar->addAction(il.load("speaker"), tr("Play Pitch Track"));
    m_playPitch->setCheckable(true);
    connect(m_playPitch, SIGNAL(triggered()), this, SLOT(playPitchToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_playPitch, SLOT(setEnabled(bool))); // JTEST: this resets the enabled state of m_playPitch.

    toolbar->addWidget(m_gainPitch);
    toolbar->addWidget(m_panPitch);

    // Notes
    QLabel *icon_notes = new QLabel;
    icon_notes->setFixedWidth(40);
    icon_notes->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    icon_notes->setPixmap(il.loadPixmap("notes"));
    toolbar->addWidget(icon_notes);

    m_showNotes = toolbar->addAction(il.load("eye"), tr("Show Notes"));
    m_showNotes->setCheckable(true);
    connect(m_showNotes, SIGNAL(triggered()), this, SLOT(showNotesToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showNotes, SLOT(setEnabled(bool)));

    m_playNotes = toolbar->addAction(il.load("speaker"), tr("Play Notes"));
    m_playNotes->setCheckable(true);
    connect(m_playNotes, SIGNAL(triggered()), this, SLOT(playNotesToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_playNotes, SLOT(setEnabled(bool)));

    toolbar->addWidget(m_gainNotes);
    toolbar->addWidget(m_panNotes);

    // Spectrogram
    QLabel *icon_spectrogram = new QLabel;
    icon_spectrogram->setFixedWidth(40);
    icon_spectrogram->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    icon_spectrogram->setPixmap(il.loadPixmap("spectrogram"));
    toolbar->addWidget(icon_spectrogram);

    m_showSpect = toolbar->addAction(il.load("eye"), tr("Show Spectrogram"));
    m_showSpect->setCheckable(true);
    connect(m_showSpect, SIGNAL(triggered()), this, SLOT(showSpectToggled()));
    connect(this, SIGNAL(canPlay(bool)), m_showSpect, SLOT(setEnabled(bool)));

    Pane::registerShortcuts(*m_keyReference);
}

void
MainWindow::toolNavigateSelected()
{
    m_viewManager->setToolMode(ViewManager::NavigateMode);
    m_intelligentActionOn = true;
}

void
MainWindow::toolEditSelected()
{
    m_viewManager->setToolMode(ViewManager::NoteEditMode);
    m_intelligentActionOn = true;
    m_analyser->setIntelligentActions(m_intelligentActionOn);
}

void
MainWindow::toolFreeEditSelected()
{
    m_viewManager->setToolMode(ViewManager::NoteEditMode);
    m_intelligentActionOn = false;
    m_analyser->setIntelligentActions(m_intelligentActionOn);
}

void
MainWindow::updateMenuStates()
{
    MainWindowBase::updateMenuStates();

    Pane *currentPane = 0;
    Layer *currentLayer = 0;

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

    bool haveCurrentPane =
        (currentPane != 0);
    bool haveCurrentLayer =
        (haveCurrentPane &&
         (currentLayer != 0));
    bool haveSelection = 
        (m_viewManager &&
         !m_viewManager->getSelections().empty());
    bool haveCurrentEditableLayer =
        (haveCurrentLayer &&
         currentLayer->isLayerEditable());
    bool haveCurrentTimeInstantsLayer = 
        (haveCurrentLayer &&
         qobject_cast<TimeInstantLayer *>(currentLayer));
    bool haveCurrentTimeValueLayer = 
        (haveCurrentLayer &&
         qobject_cast<TimeValueLayer *>(currentLayer));
    bool pitchCandidatesVisible = 
        m_analyser->arePitchCandidatesShown();
    bool haveHigher =
        m_analyser->haveHigherPitchCandidate();
    bool haveLower =
        m_analyser->haveLowerPitchCandidate();

    emit canChangePlaybackSpeed(true);
    int v = m_playSpeed->value();
    emit canSpeedUpPlayback(v < m_playSpeed->maximum());
    emit canSlowDownPlayback(v > m_playSpeed->minimum());

    emit canChangePitchCandidate(pitchCandidatesVisible && haveSelection);
    emit canChangeToHigherCandidate(pitchCandidatesVisible && haveSelection && haveHigher);
    emit canChangeToLowerCandidate(pitchCandidatesVisible && haveSelection && haveLower);

    if (pitchCandidatesVisible) {
        m_showCandidatesAction->setText(tr("Hide Pitch Candidates"));
        m_showCandidatesAction->setStatusTip(tr("Remove the display of alternate pitch candidates for the selected region"));
    } else {
        m_showCandidatesAction->setText(tr("Show Pitch Candidates"));
        m_showCandidatesAction->setStatusTip(tr("Show alternate pitch candidates for the selected region"));
    }

    if (m_ffwdAction && m_rwdAction) {
        if (haveCurrentTimeInstantsLayer) {
            m_ffwdAction->setText(tr("Fast Forward to Next Instant"));
            m_ffwdAction->setStatusTip(tr("Fast forward to the next time instant in the current layer"));
            m_rwdAction->setText(tr("Rewind to Previous Instant"));
            m_rwdAction->setStatusTip(tr("Rewind to the previous time instant in the current layer"));
        } else if (haveCurrentTimeValueLayer) {
            m_ffwdAction->setText(tr("Fast Forward to Next Point"));
            m_ffwdAction->setStatusTip(tr("Fast forward to the next point in the current layer"));
            m_rwdAction->setText(tr("Rewind to Previous Point"));
            m_rwdAction->setStatusTip(tr("Rewind to the previous point in the current layer"));
        } else {
            m_ffwdAction->setText(tr("Fast Forward"));
            m_ffwdAction->setStatusTip(tr("Fast forward"));
            m_rwdAction->setText(tr("Rewind"));
            m_rwdAction->setStatusTip(tr("Rewind"));
        }
    }
}

void
MainWindow::showAudioToggled()
{
    m_analyser->toggleVisible(Analyser::Audio);
}

void
MainWindow::showPitchToggled()
{
    m_analyser->toggleVisible(Analyser::PitchTrack);

    // JTEST
    if (!m_analyser->isVisible(Analyser::PitchTrack))
    {
        m_analyser->setAudible(Analyser::PitchTrack,false);
        m_playPitch->setChecked(false);
        m_playPitch->setEnabled(false);
    }
    else
    {
        m_playPitch->setEnabled(true);
    }
}

void
MainWindow::showSpectToggled()
{
    m_analyser->toggleVisible(Analyser::Spectrogram);
}

void
MainWindow::showNotesToggled()
{
    m_analyser->toggleVisible(Analyser::Notes);
}

void
MainWindow::playAudioToggled()
{
    m_analyser->toggleAudible(Analyser::Audio);
}

void
MainWindow::playPitchToggled()
{
    m_analyser->toggleAudible(Analyser::PitchTrack);
}

void
MainWindow::playNotesToggled()
{
    m_analyser->toggleAudible(Analyser::Notes);
}

void
MainWindow::updateLayerStatuses()
{
    m_showAudio->setChecked(m_analyser->isVisible(Analyser::Audio));
    m_showSpect->setChecked(m_analyser->isVisible(Analyser::Spectrogram));
    m_showPitch->setChecked(m_analyser->isVisible(Analyser::PitchTrack));
    m_showNotes->setChecked(m_analyser->isVisible(Analyser::Notes));
    m_playAudio->setChecked(m_analyser->isAudible(Analyser::Audio));
    m_playPitch->setChecked(m_analyser->isAudible(Analyser::PitchTrack));
    m_playNotes->setChecked(m_analyser->isAudible(Analyser::Notes));
}

void
MainWindow::updateDescriptionLabel()
{
    // Nothing, we don't have one
}

void
MainWindow::documentModified()
{
    MainWindowBase::documentModified();
}

void
MainWindow::documentRestored()
{
    MainWindowBase::documentRestored();
}

void
MainWindow::newSession()
{
    if (!checkSaveModified()) return;

    closeSession();
    createDocument();
    m_document->setAutoAlignment(true);

    Pane *pane = m_paneStack->addPane();

    connect(pane, SIGNAL(contextHelpChanged(const QString &)),
            this, SLOT(contextHelpChanged(const QString &)));

//    Layer *waveform = m_document->createMainModelLayer(LayerFactory::Waveform);
//    m_document->addLayerToView(pane, waveform);

    m_overview->registerView(pane);

    CommandHistory::getInstance()->clear();
    CommandHistory::getInstance()->documentSaved();
    documentRestored();
    updateMenuStates();
}

void
MainWindow::closeSession()
{
    if (!checkSaveModified()) return;

    while (m_paneStack->getPaneCount() > 0) {

        Pane *pane = m_paneStack->getPane(m_paneStack->getPaneCount() - 1);

        while (pane->getLayerCount() > 0) {
            m_document->removeLayerFromView
                (pane, pane->getLayer(pane->getLayerCount() - 1));
        }
        
        m_overview->unregisterView(pane);
        m_paneStack->deletePane(pane);
    }

    while (m_paneStack->getHiddenPaneCount() > 0) {

        Pane *pane = m_paneStack->getHiddenPane
            (m_paneStack->getHiddenPaneCount() - 1);
        
        while (pane->getLayerCount() > 0) {
            m_document->removeLayerFromView
                (pane, pane->getLayer(pane->getLayerCount() - 1));
        }
        
        m_overview->unregisterView(pane);
        m_paneStack->deletePane(pane);
    }

    delete m_document;
    m_document = 0;
    m_viewManager->clearSelections();
    m_timeRulerLayer = 0; // document owned this

    m_sessionFile = "";

    CommandHistory::getInstance()->clear();
    CommandHistory::getInstance()->documentSaved();
    documentRestored();
}

void
MainWindow::openFile()
{
    QString orig = m_audioFile;
    if (orig == "") orig = ".";
    else orig = QFileInfo(orig).absoluteDir().canonicalPath();

    QString path = getOpenFileName(FileFinder::AnyFile);

    if (path.isEmpty()) return;

    FileOpenStatus status = open(path, ReplaceSession);

    if (status == FileOpenFailed) {
        QMessageBox::critical(this, tr("Failed to open file"),
                              tr("<b>File open failed</b><p>File \"%1\" could not be opened").arg(path));
    } else if (status == FileOpenWrongMode) {
        QMessageBox::critical(this, tr("Failed to open file"),
                              tr("<b>Audio required</b><p>Please load at least one audio file before importing annotation data"));
    }
}

void
MainWindow::openLocation()
{
    QSettings settings;
    settings.beginGroup("MainWindow");
    QString lastLocation = settings.value("lastremote", "").toString();

    bool ok = false;
    QString text = QInputDialog::getText
        (this, tr("Open Location"),
         tr("Please enter the URL of the location to open:"),
         QLineEdit::Normal, lastLocation, &ok);

    if (!ok) return;

    settings.setValue("lastremote", text);

    if (text.isEmpty()) return;

    FileOpenStatus status = open(text, ReplaceSession);

    if (status == FileOpenFailed) {
        QMessageBox::critical(this, tr("Failed to open location"),
                              tr("<b>Open failed</b><p>URL \"%1\" could not be opened").arg(text));
    } else if (status == FileOpenWrongMode) {
        QMessageBox::critical(this, tr("Failed to open location"),
                              tr("<b>Audio required</b><p>Please load at least one audio file before importing annotation data"));
    }
}

void
MainWindow::openRecentFile()
{
    QObject *obj = sender();
    QAction *action = qobject_cast<QAction *>(obj);
    
    if (!action) {
        cerr << "WARNING: MainWindow::openRecentFile: sender is not an action"
             << endl;
        return;
    }

    QString path = action->text();
    if (path == "") return;

    FileOpenStatus status = open(path, ReplaceSession);

    if (status == FileOpenFailed) {
        QMessageBox::critical(this, tr("Failed to open location"),
                              tr("<b>Open failed</b><p>File or URL \"%1\" could not be opened").arg(path));
    } else if (status == FileOpenWrongMode) {
        QMessageBox::critical(this, tr("Failed to open location"),
                              tr("<b>Audio required</b><p>Please load at least one audio file before importing annotation data"));
    }
}

void
MainWindow::paneAdded(Pane *pane)
{
    pane->setPlaybackFollow(PlaybackScrollPage);
    m_paneStack->sizePanesEqually();
    if (m_overview) m_overview->registerView(pane);
}    

void
MainWindow::paneHidden(Pane *pane)
{
    if (m_overview) m_overview->unregisterView(pane); 
}    

void
MainWindow::paneAboutToBeDeleted(Pane *pane)
{
    if (m_overview) m_overview->unregisterView(pane); 
}    

void
MainWindow::paneDropAccepted(Pane *pane, QStringList uriList)
{
    if (pane) m_paneStack->setCurrentPane(pane);

    for (QStringList::iterator i = uriList.begin(); i != uriList.end(); ++i) {

        FileOpenStatus status = open(*i, ReplaceSession);

        if (status == FileOpenFailed) {
            QMessageBox::critical(this, tr("Failed to open dropped URL"),
                                  tr("<b>Open failed</b><p>Dropped URL \"%1\" could not be opened").arg(*i));
        } else if (status == FileOpenWrongMode) {
            QMessageBox::critical(this, tr("Failed to open dropped URL"),
                                  tr("<b>Audio required</b><p>Please load at least one audio file before importing annotation data"));
        }
    }
}

void
MainWindow::paneDropAccepted(Pane *pane, QString text)
{
    if (pane) m_paneStack->setCurrentPane(pane);

    QUrl testUrl(text);
    if (testUrl.scheme() == "file" || 
        testUrl.scheme() == "http" || 
        testUrl.scheme() == "ftp") {
        QStringList list;
        list.push_back(text);
        paneDropAccepted(pane, list);
        return;
    }

    //!!! open as text -- but by importing as if a CSV, or just adding
    //to a text layer?
}

void
MainWindow::closeEvent(QCloseEvent *e)
{
//    cerr << "MainWindow::closeEvent" << endl;

    if (m_openingAudioFile) {
//        cerr << "Busy - ignoring close event" << endl;
    e->ignore();
    return;
    }

    if (!m_abandoning && !checkSaveModified()) {
//        cerr << "Ignoring close event" << endl;
    e->ignore();
    return;
    }

    QSettings settings;
    settings.beginGroup("MainWindow");
    settings.setValue("size", size());
    settings.setValue("position", pos());
    settings.endGroup();

    delete m_keyReference;
    m_keyReference = 0;

    closeSession();

    e->accept();
    return;
}

bool
MainWindow::commitData(bool mayAskUser)
{
    if (mayAskUser) {
        bool rv = checkSaveModified();
        return rv;
    } else {
        if (!m_documentModified) return true;

        // If we can't check with the user first, then we can't save
        // to the original session file (even if we have it) -- have
        // to use a temporary file

        QString svDirBase = ".sv1";
        QString svDir = QDir::home().filePath(svDirBase);

        if (!QFileInfo(svDir).exists()) {
            if (!QDir::home().mkdir(svDirBase)) return false;
        } else {
            if (!QFileInfo(svDir).isDir()) return false;
        }
        
        // This name doesn't have to be unguessable
#ifndef _WIN32
        QString fname = QString("tmp-%1-%2.sv")
            .arg(QDateTime::currentDateTime().toString("yyyyMMddhhmmsszzz"))
            .arg(QProcess().pid());
#else
        QString fname = QString("tmp-%1.sv")
            .arg(QDateTime::currentDateTime().toString("yyyyMMddhhmmsszzz"));
#endif
        QString fpath = QDir(svDir).filePath(fname);
        if (saveSessionFile(fpath)) {
            m_recentFiles.addFile(fpath);
            return true;
        } else {
            return false;
        }
    }
}

bool
MainWindow::checkSaveModified()
{
    // Called before some destructive operation (e.g. new session,
    // exit program).  Return true if we can safely proceed, false to
    // cancel.

    if (!m_documentModified) return true;

    int button = 
    QMessageBox::warning(this,
                 tr("Session modified"),
                 tr("The current session has been modified.\nDo you want to save it?"),
                 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                             QMessageBox::Yes);

    if (button == QMessageBox::Yes) {
    saveSession();
    if (m_documentModified) { // save failed -- don't proceed!
        return false;
    } else {
            return true; // saved, so it's safe to continue now
        }
    } else if (button == QMessageBox::No) {
    m_documentModified = false; // so we know to abandon it
    return true;
    }

    // else cancel
    return false;
}

void
MainWindow::saveSession()
{
    if (m_sessionFile != "") {
    if (!saveSessionFile(m_sessionFile)) {
        QMessageBox::critical(this, tr("Failed to save file"),
                  tr("Session file \"%1\" could not be saved.").arg(m_sessionFile));
    } else {
        CommandHistory::getInstance()->documentSaved();
        documentRestored();
    }
    } else {
    saveSessionAs();
    }
}

void
MainWindow::saveSessionAs()
{
    QString orig = m_audioFile;
    if (orig == "") orig = ".";
    else orig = QFileInfo(orig).absoluteDir().canonicalPath();

    QString path = getSaveFileName(FileFinder::SessionFile);

    if (path == "") return;

    if (!saveSessionFile(path)) {
        QMessageBox::critical(this, tr("Failed to save file"),
                              tr("Session file \"%1\" could not be saved.").arg(path));
    } else {
        setWindowTitle(tr("%1: %2")
                       .arg(QApplication::applicationName())
                       .arg(QFileInfo(path).fileName()));
        m_sessionFile = path;
        CommandHistory::getInstance()->documentSaved();
        documentRestored();
        m_recentFiles.addFile(path);
    }
}

QString
MainWindow::exportToSVL(QString path, Layer *layer)
{
    Model *model = layer->getModel();

    QFile file(path);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        return tr("Failed to open file %1 for writing").arg(path);
    } else {
        QTextStream out(&file);
        out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
            << "<!DOCTYPE sonic-visualiser>\n"
            << "<sv>\n"
            << "  <data>\n";
        
        model->toXml(out, "    ");
        
        out << "  </data>\n"
            << "  <display>\n";
        
        layer->toXml(out, "    ");
        
        out << "  </display>\n"
            << "</sv>\n";

        return "";
    }
}

void
MainWindow::importPitchLayer()
{
    QString path = getOpenFileName(FileFinder::LayerFileNoMidiNonSV);
    if (path == "") return;

    FileOpenStatus status = importPitchLayer(path);

    if (status == FileOpenFailed) {
        emit hideSplash();
        QMessageBox::critical(this, tr("Failed to open file"),
                              tr("<b>File open failed</b><p>Layer file %1 could not be opened.").arg(path));
        return;
    } else if (status == FileOpenWrongMode) {
        emit hideSplash();
        QMessageBox::critical(this, tr("Failed to open file"),
                              tr("<b>Audio required</b><p>Unable to load layer data from \"%1\" without an audio file.<br>Please load at least one audio file before importing annotations.").arg(path));
    }
}

MainWindow::FileOpenStatus
MainWindow::importPitchLayer(FileSource source)
{
    if (!source.isAvailable()) return FileOpenFailed;
    source.waitForData();

    QString path = source.getLocalFilename();

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

    if (rdfType != RDFImporter::NotRDF) {

        //!!!
        return FileOpenFailed;

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

    } else {
        
        try {

            CSVFormat format(path);
            format.setSampleRate(getMainModel()->getSampleRate());

            if (format.getModelType() != CSVFormat::TwoDimensionalModel) {
                //!!! error report
                return FileOpenFailed;
            }

            Model *model = DataFileReaderFactory::loadCSV
                (path, format, getMainModel()->getSampleRate());

            if (model) {

                SVDEBUG << "MainWindow::importPitchLayer: Have model" << endl;

                CommandHistory::getInstance()->startCompoundOperation
                    (tr("Import Pitch Track"), true);

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

                m_analyser->takePitchTrackFrom(newLayer);

                m_document->deleteLayer(newLayer);

                CommandHistory::getInstance()->endCompoundOperation();

                //!!! swap all data in to existing layer instead of this

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

                return FileOpenSucceeded;
            }
        } catch (DataFileReaderFactory::Exception e) {
            if (e == DataFileReaderFactory::ImportCancelled) {
                return FileOpenCancelled;
            }
        }
    }
    
    return FileOpenFailed;
}

void
MainWindow::exportPitchLayer()
{
    Layer *layer = m_analyser->getLayer(Analyser::PitchTrack);
    if (!layer) return;

    SparseTimeValueModel *model =
        qobject_cast<SparseTimeValueModel *>(layer->getModel());
    if (!model) return;

    FileFinder::FileType type = FileFinder::LayerFileNoMidiNonSV;

    QString path = getSaveFileName(type);

    if (path == "") return;

    if (QFileInfo(path).suffix() == "") path += ".svl";

    QString suffix = QFileInfo(path).suffix().toLower();

    QString error;

    if (suffix == "xml" || suffix == "svl") {

        error = exportToSVL(path, layer);

    } else if (suffix == "ttl" || suffix == "n3") {

        RDFExporter exporter(path, model);
        exporter.write();
        if (!exporter.isOK()) {
            error = exporter.getError();
        }

    } else {

        CSVFileWriter writer(path, model,
                             ((suffix == "csv") ? "," : "\t"));
        writer.write();

        if (!writer.isOK()) {
            error = writer.getError();
        }
    }

    if (error != "") {
        QMessageBox::critical(this, tr("Failed to write file"), error);
    } else {
        emit activity(tr("Export layer to \"%1\"").arg(path));
    }
}

void
MainWindow::exportNoteLayer()
{
    Layer *layer = m_analyser->getLayer(Analyser::Notes);
    if (!layer) return;

    FlexiNoteModel *model = qobject_cast<FlexiNoteModel *>(layer->getModel());
    if (!model) return;

    FileFinder::FileType type = FileFinder::LayerFileNonSV;

    QString path = getSaveFileName(type);

    if (path == "") return;

    if (QFileInfo(path).suffix() == "") path += ".svl";

    QString suffix = QFileInfo(path).suffix().toLower();

    QString error;

    if (suffix == "xml" || suffix == "svl") {

        error = exportToSVL(path, layer);

    } else if (suffix == "mid" || suffix == "midi") {
     
        MIDIFileWriter writer(path, model, model->getSampleRate());
        writer.write();
        if (!writer.isOK()) {
            error = writer.getError();
        }

    } else if (suffix == "ttl" || suffix == "n3") {

        RDFExporter exporter(path, model);
        exporter.write();
        if (!exporter.isOK()) {
            error = exporter.getError();
        }

    } else {

        CSVFileWriter writer(path, model,
                             ((suffix == "csv") ? "," : "\t"));
        writer.write();

        if (!writer.isOK()) {
            error = writer.getError();
        }
    }

    if (error != "") {
        QMessageBox::critical(this, tr("Failed to write file"), error);
    } else {
        emit activity(tr("Export layer to \"%1\"").arg(path));
    }
}

void
MainWindow::doubleClickSelectInvoked(size_t frame)
{
    size_t f0, f1;
    m_analyser->getEnclosingSelectionScope(frame, f0, f1);
    
    cerr << "MainWindow::doubleClickSelectInvoked(" << frame << "): [" << f0 << "," << f1 << "]" << endl;

    Selection sel(f0, f1);
    m_viewManager->setSelection(sel);
}

void
MainWindow::abandonSelection()
{
    // Named abandonSelection rather than clearSelection to indicate
    // that this is an active operation -- it restores the original
    // content of the pitch track in the selected region rather than
    // simply un-selecting.

    cerr << "MainWindow::abandonSelection()" << endl;

    CommandHistory::getInstance()->startCompoundOperation(tr("Abandon Selection"), true);

    MultiSelection::SelectionList selections = m_viewManager->getSelections();
    if (!selections.empty()) {
        Selection sel = *selections.begin();
        m_analyser->abandonReAnalysis(sel);
    }

    MainWindowBase::clearSelection();

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

void
MainWindow::selectionChangedByUser()
{
    MultiSelection::SelectionList selections = m_viewManager->getSelections();

    cerr << "MainWindow::selectionChangedByUser" << endl;

    m_analyser->showPitchCandidates(m_pendingConstraint.isConstrained());

    if (!selections.empty()) {
        Selection sel = *selections.begin();
        cerr << "MainWindow::selectionChangedByUser: have selection" << endl;
        QString error = m_analyser->reAnalyseSelection
            (sel, m_pendingConstraint);
        if (error != "") {
            QMessageBox::critical
                (this, tr("Failed to analyse selection"),
                 tr("<b>Analysis failed</b><p>%2</p>").arg(error));
        }
    }

    m_pendingConstraint = Analyser::FrequencyRange();
}

void
MainWindow::regionOutlined(QRect r)
{
    cerr << "MainWindow::regionOutlined(" << r.x() << "," << r.y() << "," << r.width() << "," << r.height() << ")" << endl;

    Pane *pane = qobject_cast<Pane *>(sender());
    if (!pane) {
        cerr << "MainWindow::regionOutlined: not sent by pane, ignoring" << endl;
        return;
    }

    if (!m_analyser) {
        cerr << "MainWindow::regionOutlined: no analyser, ignoring" << endl;
        return;
    }

    SpectrogramLayer *spectrogram = qobject_cast<SpectrogramLayer *>
        (m_analyser->getLayer(Analyser::Spectrogram));
    if (!spectrogram) {
        cerr << "MainWindow::regionOutlined: no spectrogram layer, ignoring" << endl;
        return;
    }

    int f0 = pane->getFrameForX(r.x());
    int f1 = pane->getFrameForX(r.x() + r.width());
    
    float v0 = spectrogram->getFrequencyForY(pane, r.y() + r.height());
    float v1 = spectrogram->getFrequencyForY(pane, r.y());

    cerr << "MainWindow::regionOutlined: frame " << f0 << " -> " << f1 
         << ", frequency " << v0 << " -> " << v1 << endl;

    m_pendingConstraint = Analyser::FrequencyRange(v0, v1);

    Selection sel(f0, f1);
    m_viewManager->setSelection(sel);
}

void
MainWindow::clearPitches()
{
    MultiSelection::SelectionList selections = m_viewManager->getSelections();

    CommandHistory::getInstance()->startCompoundOperation(tr("Clear Pitches"), true);

    for (MultiSelection::SelectionList::iterator k = selections.begin();
         k != selections.end(); ++k) {
        m_analyser->deletePitches(*k);
    }

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

void
MainWindow::octaveShiftUp()
{
    octaveShift(true);
}

void
MainWindow::octaveShiftDown()
{
    octaveShift(false);
}

void
MainWindow::octaveShift(bool up)
{
    MultiSelection::SelectionList selections = m_viewManager->getSelections();

    CommandHistory::getInstance()->startCompoundOperation(tr("Octave Shift"), true);

    for (MultiSelection::SelectionList::iterator k = selections.begin();
         k != selections.end(); ++k) {

        m_analyser->shiftOctave(*k, up);
    }

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

void
MainWindow::togglePitchCandidates()
{
    CommandHistory::getInstance()->startCompoundOperation(tr("Toggle Pitch Candidates"), true);

    m_analyser->showPitchCandidates(!m_analyser->arePitchCandidatesShown());

    CommandHistory::getInstance()->endCompoundOperation();

    updateMenuStates();
}

void
MainWindow::switchPitchUp()
{
    CommandHistory::getInstance()->startCompoundOperation
        (tr("Switch Pitch Candidate"), true);

    MultiSelection::SelectionList selections = m_viewManager->getSelections();

    for (MultiSelection::SelectionList::iterator k = selections.begin();
         k != selections.end(); ++k) {
        m_analyser->switchPitchCandidate(*k, true);
    }

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

void
MainWindow::switchPitchDown()
{
    CommandHistory::getInstance()->startCompoundOperation
        (tr("Switch Pitch Candidate"), true);

    MultiSelection::SelectionList selections = m_viewManager->getSelections();

    for (MultiSelection::SelectionList::iterator k = selections.begin();
         k != selections.end(); ++k) {
        m_analyser->switchPitchCandidate(*k, false);
    }

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

void
MainWindow::playSpeedChanged(int position)
{
    PlaySpeedRangeMapper mapper(0, 200);

    float percent = m_playSpeed->mappedValue();
    float factor = mapper.getFactorForValue(percent);

    cerr << "speed = " << position << " percent = " << percent << " factor = " << factor << endl;

    bool something = (position != 100);

    int pc = lrintf(percent);

    if (!something) {
        contextHelpChanged(tr("Playback speed: Normal"));
    } else {
        contextHelpChanged(tr("Playback speed: %1%2%")
                           .arg(position > 100 ? "+" : "")
                           .arg(pc));
    }

    m_playSource->setTimeStretch(factor);

    updateMenuStates();
}

void
MainWindow::playSharpenToggled()
{
    QSettings settings;
    settings.beginGroup("MainWindow");
    settings.setValue("playsharpen", m_playSharpen->isChecked());
    settings.endGroup();

    playSpeedChanged(m_playSpeed->value());
    // TODO: pitch gain?
}

void
MainWindow::playMonoToggled()
{
    QSettings settings;
    settings.beginGroup("MainWindow");
    settings.setValue("playmono", m_playMono->isChecked());
    settings.endGroup();

    playSpeedChanged(m_playSpeed->value());
    // TODO: pitch gain?
}    

void
MainWindow::speedUpPlayback()
{
    int value = m_playSpeed->value();
    value = value + m_playSpeed->pageStep();
    if (value > m_playSpeed->maximum()) value = m_playSpeed->maximum();
    m_playSpeed->setValue(value);
}

void
MainWindow::slowDownPlayback()
{
    int value = m_playSpeed->value();
    value = value - m_playSpeed->pageStep();
    if (value < m_playSpeed->minimum()) value = m_playSpeed->minimum();
    m_playSpeed->setValue(value);
}

void
MainWindow::restoreNormalPlayback()
{
    m_playSpeed->setValue(m_playSpeed->defaultValue());
}

void
MainWindow::audioGainChanged(int position)
{
    float level = m_gainAudio->mappedValue();
    float gain = powf(10, level / 20.0);

    cerr << "gain = " << gain << " (" << position << " dB)" << endl;

    contextHelpChanged(tr("Audio Gain: %1 dB").arg(position));

    m_analyser->setGain(Analyser::Audio, gain);

    updateMenuStates();
} 

void
MainWindow::increaseAudioGain()
{
    int value = m_gainAudio->value();
    value = value + m_gainAudio->pageStep();
    if (value > m_gainAudio->maximum()) value = m_gainAudio->maximum();
    m_gainAudio->setValue(value);
}

void
MainWindow::decreaseAudioGain()
{
    int value = m_gainAudio->value();
    value = value - m_gainAudio->pageStep();
    if (value < m_gainAudio->minimum()) value = m_gainAudio->minimum();
    m_gainAudio->setValue(value);
}

void
MainWindow::restoreNormalAudioGain()
{
    m_gainAudio->setValue(m_gainAudio->defaultValue());
}

void
MainWindow::pitchGainChanged(int position)
{
    float level = m_gainPitch->mappedValue();
    float gain = powf(10, level / 20.0);

    cerr << "gain = " << gain << " (" << position << " dB)" << endl;

    contextHelpChanged(tr("Pitch Gain: %1 dB").arg(position));

    m_analyser->setGain(Analyser::PitchTrack, gain);

    updateMenuStates();
} 

void
MainWindow::increasePitchGain()
{
    int value = m_gainPitch->value();
    value = value + m_gainPitch->pageStep();
    if (value > m_gainPitch->maximum()) value = m_gainPitch->maximum();
    m_gainPitch->setValue(value);
}

void
MainWindow::decreasePitchGain()
{
    int value = m_gainPitch->value();
    value = value - m_gainPitch->pageStep();
    if (value < m_gainPitch->minimum()) value = m_gainPitch->minimum();
    m_gainPitch->setValue(value);
}

void
MainWindow::restoreNormalPitchGain()
{
    m_gainPitch->setValue(m_gainPitch->defaultValue());
}

void
MainWindow::notesGainChanged(int position)
{
    float level = m_gainNotes->mappedValue();
    float gain = powf(10, level / 20.0);

    cerr << "gain = " << gain << " (" << position << " dB)" << endl;

    contextHelpChanged(tr("Notes Gain: %1 dB").arg(position));

    m_analyser->setGain(Analyser::Notes, gain);

    updateMenuStates();
} 

void
MainWindow::increaseNotesGain()
{
    int value = m_gainNotes->value();
    value = value + m_gainNotes->pageStep();
    if (value > m_gainNotes->maximum()) value = m_gainNotes->maximum();
    m_gainNotes->setValue(value);
}

void
MainWindow::decreaseNotesGain()
{
    int value = m_gainNotes->value();
    value = value - m_gainNotes->pageStep();
    if (value < m_gainNotes->minimum()) value = m_gainNotes->minimum();
    m_gainNotes->setValue(value);
}

void
MainWindow::restoreNormalNotesGain()
{
    m_gainNotes->setValue(m_gainNotes->defaultValue());
}

void
MainWindow::audioPanChanged(int position)
{
    float level = m_panAudio->mappedValue();
    float pan = level/100.f;

    cerr << "pan = " << pan << " (" << position << ")" << endl;

    contextHelpChanged(tr("Audio Pan: %1").arg(position));

    m_analyser->setPan(Analyser::Audio, pan);

    updateMenuStates();
} 

void
MainWindow::increaseAudioPan()
{
    int value = m_panAudio->value();
    value = value + m_panAudio->pageStep();
    if (value > m_panAudio->maximum()) value = m_panAudio->maximum();
    m_panAudio->setValue(value);
}

void
MainWindow::decreaseAudioPan()
{
    int value = m_panAudio->value();
    value = value - m_panAudio->pageStep();
    if (value < m_panAudio->minimum()) value = m_panAudio->minimum();
    m_panAudio->setValue(value);
}

void
MainWindow::restoreNormalAudioPan()
{
    m_panAudio->setValue(m_panAudio->defaultValue());
}

void
MainWindow::pitchPanChanged(int position)
{
    float level = m_panPitch->mappedValue();
    float pan = level/100.f;

    cerr << "pan = " << pan << " (" << position << ")" << endl;

    contextHelpChanged(tr("Pitch Pan: %1").arg(position));

    m_analyser->setPan(Analyser::PitchTrack, pan);

    updateMenuStates();
} 

void
MainWindow::increasePitchPan()
{
    int value = m_panPitch->value();
    value = value + m_panPitch->pageStep();
    if (value > m_panPitch->maximum()) value = m_panPitch->maximum();
    m_panPitch->setValue(value);
}

void
MainWindow::decreasePitchPan()
{
    int value = m_panPitch->value();
    value = value - m_panPitch->pageStep();
    if (value < m_panPitch->minimum()) value = m_panPitch->minimum();
    m_panPitch->setValue(value);
}

void
MainWindow::restoreNormalPitchPan()
{
    m_panPitch->setValue(m_panPitch->defaultValue());
}

void
MainWindow::notesPanChanged(int position)
{
    float level = m_panNotes->mappedValue();
    float pan = level/100.f;

    cerr << "pan = " << pan << " (" << position << ")" << endl;

    contextHelpChanged(tr("Notes Pan: %1").arg(position));

    m_analyser->setPan(Analyser::Notes, pan);

    updateMenuStates();
} 

void
MainWindow::increaseNotesPan()
{
    int value = m_panNotes->value();
    value = value + m_panNotes->pageStep();
    if (value > m_panNotes->maximum()) value = m_panNotes->maximum();
    m_panNotes->setValue(value);
}

void
MainWindow::decreaseNotesPan()
{
    int value = m_panNotes->value();
    value = value - m_panNotes->pageStep();
    if (value < m_panNotes->minimum()) value = m_panNotes->minimum();
    m_panNotes->setValue(value);
}

void
MainWindow::restoreNormalNotesPan()
{
    m_panNotes->setValue(m_panNotes->defaultValue());
}

void
MainWindow::updateVisibleRangeDisplay(Pane *p) const
{
    if (!getMainModel() || !p) {
        return;
    }

    bool haveSelection = false;
    size_t startFrame = 0, endFrame = 0;

    if (m_viewManager && m_viewManager->haveInProgressSelection()) {

        bool exclusive = false;
        Selection s = m_viewManager->getInProgressSelection(exclusive);

        if (!s.isEmpty()) {
            haveSelection = true;
            startFrame = s.getStartFrame();
            endFrame = s.getEndFrame();
        }
    }

    if (!haveSelection) {
        startFrame = p->getFirstVisibleFrame();
        endFrame = p->getLastVisibleFrame();
    }

    RealTime start = RealTime::frame2RealTime
        (startFrame, getMainModel()->getSampleRate());

    RealTime end = RealTime::frame2RealTime
        (endFrame, getMainModel()->getSampleRate());

    RealTime duration = end - start;

    QString startStr, endStr, durationStr;
    startStr = start.toText(true).c_str();
    endStr = end.toText(true).c_str();
    durationStr = duration.toText(true).c_str();

    if (haveSelection) {
        m_myStatusMessage = tr("Selection: %1 to %2 (duration %3)")
            .arg(startStr).arg(endStr).arg(durationStr);
    } else {
        m_myStatusMessage = tr("Visible: %1 to %2 (duration %3)")
            .arg(startStr).arg(endStr).arg(durationStr);
    }
    
    // scale Y axis
    FlexiNoteLayer *fnl = dynamic_cast<FlexiNoteLayer *>(p->getLayer(2));
    if (fnl) {
        fnl->setVerticalRangeToNoteRange(p);
    }
    
    statusBar()->showMessage(m_myStatusMessage);
}

void
MainWindow::updatePositionStatusDisplays() const
{
    if (!statusBar()->isVisible()) return;

}

void
MainWindow::outputLevelsChanged(float left, float right)
{
    m_fader->setPeakLeft(left);
    m_fader->setPeakRight(right);
}

void
MainWindow::sampleRateMismatch(size_t requested, size_t actual,
                               bool willResample)
{
    updateDescriptionLabel();
}

void
MainWindow::audioOverloadPluginDisabled()
{
    QMessageBox::information
        (this, tr("Audio processing overload"),
         tr("<b>Overloaded</b><p>Audio effects plugin auditioning has been disabled due to a processing overload."));
}

void
MainWindow::audioTimeStretchMultiChannelDisabled()
{
    static bool shownOnce = false;
    if (shownOnce) return;
    QMessageBox::information
        (this, tr("Audio processing overload"),
         tr("<b>Overloaded</b><p>Audio playback speed processing has been reduced to a single channel, due to a processing overload."));
    shownOnce = true;
}

void
MainWindow::layerRemoved(Layer *layer)
{
    MainWindowBase::layerRemoved(layer);
}

void
MainWindow::layerInAView(Layer *layer, bool inAView)
{
    MainWindowBase::layerInAView(layer, inAView);
}

void
MainWindow::modelAdded(Model *model)
{
    MainWindowBase::modelAdded(model);
    DenseTimeValueModel *dtvm = qobject_cast<DenseTimeValueModel *>(model);
    if (dtvm) {
        cerr << "A dense time-value model (such as an audio file) has been loaded" << endl;
    }
}

void
MainWindow::modelAboutToBeDeleted(Model *model)
{
    MainWindowBase::modelAboutToBeDeleted(model);
}

void
MainWindow::mainModelChanged(WaveFileModel *model)
{
    m_panLayer->setModel(model);

    MainWindowBase::mainModelChanged(model);

    if (m_playTarget) {
        connect(m_fader, SIGNAL(valueChanged(float)),
                m_playTarget, SLOT(setOutputGain(float)));
    }

    if (model) {
        if (m_paneStack) {

            int pc = m_paneStack->getPaneCount();
            Pane *pane = 0;

            if (pc < 1) {
                pane = m_paneStack->addPane();

                Pane *selectionStrip = m_paneStack->addPane();
                selectionStrip->setFixedHeight(26);
                m_document->addLayerToView
                    (selectionStrip,
                     m_document->createMainModelLayer(LayerFactory::TimeRuler));
                m_paneStack->sizePanesEqually();

                m_viewManager->clearToolModeOverrides();
                m_viewManager->setToolModeFor(selectionStrip,
                                              ViewManager::SelectMode);
            } else {
                pane = m_paneStack->getPane(0);
            }

            if (pane) {

                disconnect(pane, SIGNAL(regionOutlined(QRect)),
                           pane, SLOT(zoomToRegion(QRect)));
                connect(pane, SIGNAL(regionOutlined(QRect)),
                        this, SLOT(regionOutlined(QRect)));

                QString error = m_analyser->newFileLoaded
                    (m_document, getMainModel(), m_paneStack, pane);
                if (error != "") {
                    QMessageBox::warning
                        (this,
                         tr("Failed to analyse audio"),
                         tr("<b>Analysis failed</b><p>%1</p>").arg(error),
                         QMessageBox::Ok);
                }
            }
        }
    }
}

void
MainWindow::modelGenerationFailed(QString transformName, QString message)
{
    if (message != "") {

        QMessageBox::warning
            (this,
             tr("Failed to generate layer"),
             tr("<b>Layer generation failed</b><p>Failed to generate derived layer.<p>The layer transform \"%1\" failed:<p>%2")
             .arg(transformName).arg(message),
             QMessageBox::Ok);
    } else {
        QMessageBox::warning
            (this,
             tr("Failed to generate layer"),
             tr("<b>Layer generation failed</b><p>Failed to generate a derived layer.<p>The layer transform \"%1\" failed.<p>No error information is available.")
             .arg(transformName),
             QMessageBox::Ok);
    }
}

void
MainWindow::modelGenerationWarning(QString transformName, QString message)
{
    QMessageBox::warning
        (this, tr("Warning"), message, QMessageBox::Ok);
}

void
MainWindow::modelRegenerationFailed(QString layerName,
                                    QString transformName, QString message)
{
    if (message != "") {

        QMessageBox::warning
            (this,
             tr("Failed to regenerate layer"),
             tr("<b>Layer generation failed</b><p>Failed to regenerate derived layer \"%1\" using new data model as input.<p>The layer transform \"%2\" failed:<p>%3")
             .arg(layerName).arg(transformName).arg(message),
             QMessageBox::Ok);
    } else {
        QMessageBox::warning
            (this,
             tr("Failed to regenerate layer"),
             tr("<b>Layer generation failed</b><p>Failed to regenerate derived layer \"%1\" using new data model as input.<p>The layer transform \"%2\" failed.<p>No error information is available.")
             .arg(layerName).arg(transformName),
             QMessageBox::Ok);
    }
}

void
MainWindow::modelRegenerationWarning(QString layerName,
                                     QString transformName, QString message)
{
    QMessageBox::warning
        (this, tr("Warning"), tr("<b>Warning when regenerating layer</b><p>When regenerating the derived layer \"%1\" using new data model as input:<p>%2").arg(layerName).arg(message), QMessageBox::Ok);
}

void
MainWindow::alignmentFailed(QString transformName, QString message)
{
    QMessageBox::warning
        (this,
         tr("Failed to calculate alignment"),
         tr("<b>Alignment calculation failed</b><p>Failed to calculate an audio alignment using transform \"%1\":<p>%2")
         .arg(transformName).arg(message),
         QMessageBox::Ok);
}

void
MainWindow::rightButtonMenuRequested(Pane *pane, QPoint position)
{
//    cerr << "MainWindow::rightButtonMenuRequested(" << pane << ", " << position.x() << ", " << position.y() << ")" << endl;
    m_paneStack->setCurrentPane(pane);
    m_rightButtonMenu->popup(position);
}

void
MainWindow::handleOSCMessage(const OSCMessage &message)
{
    cerr << "MainWindow::handleOSCMessage: Not implemented" << endl;
}

void
MainWindow::mouseEnteredWidget()
{
    QWidget *w = qobject_cast<QWidget *>(sender());
    if (!w) return;

    if (w == m_fader) {
        contextHelpChanged(tr("Adjust the master playback level"));
    } else if (w == m_playSpeed) {
        contextHelpChanged(tr("Adjust the master playback speed"));
    } else if (w == m_playSharpen && w->isEnabled()) {
        contextHelpChanged(tr("Toggle transient sharpening for playback time scaling"));
    } else if (w == m_playMono && w->isEnabled()) {
        contextHelpChanged(tr("Toggle mono mode for playback time scaling"));
    }
}

void
MainWindow::mouseLeftWidget()
{
    contextHelpChanged("");
}

void
MainWindow::website()
{
    //!!! todo: URL!
    openHelpUrl(tr("http://code.soundsoftware.ac.uk/projects/tony/"));
}

void
MainWindow::help()
{
    //!!! todo: help URL!
    openHelpUrl(tr("http://code.soundsoftware.ac.uk/projects/tony/"));
}

void
MainWindow::about()
{
    bool debug = false;
    QString version = "(unknown version)";

#ifdef BUILD_DEBUG
    debug = true;
#endif
    version = tr("Release %1").arg(TONY_VERSION);

    QString aboutText;

    aboutText += tr("<h3>About Tony</h3>");
    aboutText += tr("<p>Tony is a program for interactive note and pitch analysis and annotation.</p>");
    aboutText += tr("<p>%1 : %2 configuration</p>")
        .arg(version)
        .arg(debug ? tr("Debug") : tr("Release"));
    aboutText += tr("<p>Using Qt version %1</p>")
        .arg(QT_VERSION_STR);

    aboutText += 
        "<p>Copyright &copy; 2005&ndash;2013 Chris Cannam, Matthias Mauch, George Fazekas, and Queen Mary University of London.</p>"
        "<p>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.<br>See the file "
        "COPYING included with this distribution for more information.</p>";
    
    QMessageBox::about(this, tr("About %1").arg(QApplication::applicationName()), aboutText);
}

void
MainWindow::keyReference()
{
    m_keyReference->show();
}