view view/Pane.cpp @ 1496:d09345e578a7

Separate out handling of alignment progress bar from the layer progress bars and fix tendency to have them hanging around even when alignment has completed
author Chris Cannam
date Wed, 14 Aug 2019 10:58:24 +0100
parents c83504eb2649
children 175770a13495
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 "Pane.h"
#include "layer/Layer.h"
#include "data/model/Model.h"
#include "base/ZoomConstraint.h"
#include "base/RealTime.h"
#include "base/Profiler.h"
#include "ViewManager.h"
#include "widgets/CommandHistory.h"
#include "widgets/TextAbbrev.h"
#include "widgets/IconLoader.h"
#include "base/Preferences.h"
#include "layer/WaveformLayer.h"
#include "layer/TimeRulerLayer.h"
#include "layer/PaintAssistant.h"

// GF: added so we can propagate the mouse move event to the note layer for context handling.
#include "layer/LayerFactory.h"
#include "layer/FlexiNoteLayer.h"


//!!! ugh
#include "data/model/WaveFileModel.h"

#include <QPaintEvent>
#include <QPainter>
#include <QBitmap>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QCursor>
#include <QTextStream>
#include <QMimeData>
#include <QApplication>

#include <iostream>
#include <cmath>

//!!! for HUD -- pull out into a separate class
#include <QFrame>
#include <QGridLayout>
#include <QPushButton>
#include "widgets/Thumbwheel.h"
#include "widgets/Panner.h"
#include "widgets/RangeInputDialog.h"
#include "widgets/NotifyingPushButton.h"

#include "widgets/KeyReference.h" //!!! should probably split KeyReference into a data class in base and another that shows the widget

//#define DEBUG_PANE

QCursor *Pane::m_measureCursor1 = nullptr;
QCursor *Pane::m_measureCursor2 = nullptr;

Pane::Pane(QWidget *w) :
    View(w, true),
    m_identifyFeatures(false),
    m_clickedInRange(false),
    m_shiftPressed(false),
    m_ctrlPressed(false),
    m_altPressed(false),
    m_navigating(false),
    m_resizing(false),
    m_editing(false),
    m_releasing(false),
    m_centreLineVisible(true),
    m_scaleWidth(0),
    m_pendingWheelAngle(0),
    m_headsUpDisplay(nullptr),
    m_vpan(nullptr),
    m_hthumb(nullptr),
    m_vthumb(nullptr),
    m_reset(nullptr),
    m_mouseInWidget(false),
    m_playbackFrameMoveScheduled(false),
    m_playbackFrameMoveTo(0)
{
    setObjectName("Pane");
    setMouseTracking(true);
    setAcceptDrops(true);
    
    updateHeadsUpDisplay();

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

    cerr << "Pane::Pane(" << this << ") returning" << endl;
}

void
Pane::updateHeadsUpDisplay()
{
    Profiler profiler("Pane::updateHeadsUpDisplay");

    if (!isVisible()) return;

    Layer *layer = nullptr;
    if (getLayerCount() > 0) {
        layer = getLayer(getLayerCount() - 1);
    }

    if (!m_headsUpDisplay) {

        m_headsUpDisplay = new QFrame(this);

        QGridLayout *layout = new QGridLayout;
        layout->setMargin(0);
        layout->setSpacing(0);
        m_headsUpDisplay->setLayout(layout);
        
        m_hthumb = new Thumbwheel(Qt::Horizontal);
        m_hthumb->setObjectName(tr("Horizontal Zoom"));
        m_hthumb->setCursor(Qt::ArrowCursor);
        layout->addWidget(m_hthumb, 1, 0, 1, 2);
        m_hthumb->setFixedWidth(m_manager->scalePixelSize(70));
        m_hthumb->setFixedHeight(m_manager->scalePixelSize(16));
        m_hthumb->setDefaultValue(0);
        m_hthumb->setSpeed(0.6f);
        connect(m_hthumb, SIGNAL(valueChanged(int)), this, 
                SLOT(horizontalThumbwheelMoved(int)));
        connect(m_hthumb, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
        connect(m_hthumb, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

        m_vpan = new Panner;
        m_vpan->setCursor(Qt::ArrowCursor);
        layout->addWidget(m_vpan, 0, 1);
        m_vpan->setFixedWidth(m_manager->scalePixelSize(12));
        m_vpan->setFixedHeight(m_manager->scalePixelSize(70));
        m_vpan->setAlpha(80, 130);
        connect(m_vpan, SIGNAL(rectExtentsChanged(float, float, float, float)),
                this, SLOT(verticalPannerMoved(float, float, float, float)));
        connect(m_vpan, SIGNAL(doubleClicked()),
                this, SLOT(editVerticalPannerExtents()));
        connect(m_vpan, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
        connect(m_vpan, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

        m_vthumb = new Thumbwheel(Qt::Vertical);
        m_vthumb->setObjectName(tr("Vertical Zoom"));
        m_vthumb->setCursor(Qt::ArrowCursor);
        layout->addWidget(m_vthumb, 0, 2);
        m_vthumb->setFixedWidth(m_manager->scalePixelSize(16));
        m_vthumb->setFixedHeight(m_manager->scalePixelSize(70));
        connect(m_vthumb, SIGNAL(valueChanged(int)), this, 
                SLOT(verticalThumbwheelMoved(int)));
        connect(m_vthumb, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
        connect(m_vthumb, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));

        if (layer) {
            RangeMapper *rm = layer->getNewVerticalZoomRangeMapper();
            if (rm) m_vthumb->setRangeMapper(rm);
        }

        m_reset = new NotifyingPushButton;
        m_reset->setFlat(true);
        m_reset->setCursor(Qt::ArrowCursor);
        m_reset->setFixedHeight(m_manager->scalePixelSize(16));
        m_reset->setFixedWidth(m_manager->scalePixelSize(16));
        m_reset->setIcon(IconLoader().load("zoom-reset"));
        m_reset->setToolTip(tr("Reset zoom to default"));
        layout->addWidget(m_reset, 1, 2);
        
        layout->setColumnStretch(0, 20);

        connect(m_reset, SIGNAL(clicked()), m_hthumb, SLOT(resetToDefault()));
        connect(m_reset, SIGNAL(clicked()), m_vthumb, SLOT(resetToDefault()));
        connect(m_reset, SIGNAL(clicked()), m_vpan, SLOT(resetToDefault()));
        connect(m_reset, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
        connect(m_reset, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));
    }

    int count = countZoomLevels();
    int current = getZoomLevelIndex(getZoomLevel());
    
    m_hthumb->setMinimumValue(1);
    m_hthumb->setMaximumValue(count);
    m_hthumb->setValue(count - current);

    if (m_hthumb->getDefaultValue() == 0) {
        m_hthumb->setDefaultValue(count - current);
    }

    bool haveVThumb = false;

    if (layer) {
        int defaultStep = 0;
        int max = layer->getVerticalZoomSteps(defaultStep);
        if (max == 0) {
            m_vthumb->hide();
        } else {
            haveVThumb = true;
            m_vthumb->show();
            m_vthumb->blockSignals(true);
            m_vthumb->setMinimumValue(0);
            m_vthumb->setMaximumValue(max);
            m_vthumb->setDefaultValue(defaultStep);
            m_vthumb->setValue(layer->getCurrentVerticalZoomStep());
            m_vthumb->blockSignals(false);

//            cerr << "Vertical thumbwheel: min 0, max " << max
//                      << ", default " << defaultStep << ", value "
//                      << m_vthumb->getValue() << endl;

        }
    }

    updateVerticalPanner();

    if (m_manager && m_manager->getZoomWheelsEnabled() &&
        width() > m_manager->scalePixelSize(120) &&
        height() > m_manager->scalePixelSize(100)) {
        if (!m_headsUpDisplay->isVisible()) {
            m_headsUpDisplay->show();
        }
        int shift = m_manager->scalePixelSize(86);
        if (haveVThumb) {
            m_headsUpDisplay->setFixedHeight(m_vthumb->height() + m_hthumb->height());
            m_headsUpDisplay->move(width() - shift, height() - shift);
        } else {
            m_headsUpDisplay->setFixedHeight(m_hthumb->height());
            m_headsUpDisplay->move(width() - shift,
                                   height() - m_manager->scalePixelSize(16));
        }
    } else {
        m_headsUpDisplay->hide();
    }
}

void
Pane::updateVerticalPanner()
{
    if (!m_vpan || !m_manager || !m_manager->getZoomWheelsEnabled()) return;

    // In principle we should show or hide the panner on the basis of
    // whether the top layer has adjustable display extents, and we do
    // that below.  However, we have no basis for layout of the panner
    // if the vertical scroll wheel is not also present.  So if we
    // have no vertical scroll wheel, we should remove the panner as
    // well.  Ideally any layer that implements display extents should
    // implement vertical zoom steps as well, but they don't all at
    // the moment.

    Layer *layer = nullptr;
    if (getLayerCount() > 0) layer = getLayer(getLayerCount() - 1);
    int discard;
    if (layer && layer->getVerticalZoomSteps(discard) == 0) {
        m_vpan->hide();
        return;
    }

    double vmin, vmax, dmin, dmax;
    if (getTopLayerDisplayExtents(vmin, vmax, dmin, dmax) && vmax != vmin) {
        double y0 = (dmin - vmin) / (vmax - vmin);
        double y1 = (dmax - vmin) / (vmax - vmin);
        m_vpan->blockSignals(true);
        m_vpan->setRectExtents(0, float(1.0 - y1), 1, float(y1 - y0));
        m_vpan->blockSignals(false);
        m_vpan->show();
    } else {
        m_vpan->hide();
    }
}

bool
Pane::shouldIlluminateLocalFeatures(const Layer *layer, QPoint &pos) const
{
    QPoint discard;
    bool b0, b1;

    if (m_manager && m_manager->getToolModeFor(this) == ViewManager::MeasureMode) {
        return false;
    }
    
    if (m_manager && !m_manager->shouldIlluminateLocalFeatures()) {
        return false;
    }

    if (layer == getInteractionLayer() &&
        !shouldIlluminateLocalSelection(discard, b0, b1)) {

        pos = m_identifyPoint;
        return m_identifyFeatures;
    }

    return false;
}

bool
Pane::shouldIlluminateLocalSelection(QPoint &pos,
                                     bool &closeToLeft,
                                     bool &closeToRight) const
{
    if (m_identifyFeatures &&
        m_manager &&
        m_manager->getToolModeFor(this) == ViewManager::EditMode &&
        !m_manager->getSelections().empty() &&
        !selectionIsBeingEdited()) {

        Selection s(getSelectionAt(m_identifyPoint.x(),
                                   closeToLeft, closeToRight));

        if (!s.isEmpty()) {
            if (getInteractionLayer() && getInteractionLayer()->isLayerEditable()) {
            
                pos = m_identifyPoint;
                return true;
            }
        }
    }

    return false;
}

bool
Pane::selectionIsBeingEdited() const
{
    if (!m_editingSelection.isEmpty()) {
        if (m_mousePos != m_clickPos &&
            getFrameForX(m_mousePos.x()) != getFrameForX(m_clickPos.x())) {
            return true;
        }
    }
    return false;
}

void
Pane::setCentreLineVisible(bool visible)
{
    m_centreLineVisible = visible;
    update();
}

void
Pane::paintEvent(QPaintEvent *e)
{
//    Profiler profiler("Pane::paintEvent", true);

    QPainter paint;

    QRect r(rect());
    if (e) r = e->rect();

    View::paintEvent(e);

    paint.begin(this);
    setPaintFont(paint);

    if (e) paint.setClipRect(r);

    ViewManager::ToolMode toolMode = ViewManager::NavigateMode;
    if (m_manager) toolMode = m_manager->getToolModeFor(this);

    // Locate some relevant layers and models
    
    Layer *topLayer = getTopLayer();
    bool haveSomeTimeXAxis = false;

    ModelId waveformModelId; // just for reporting purposes
    ModelId workModelId;

    for (LayerList::iterator vi = m_layerStack.end();
         vi != m_layerStack.begin(); ) {

        --vi;

        if (!haveSomeTimeXAxis && (*vi)->hasTimeXAxis()) {
            haveSomeTimeXAxis = true;
        }

        ModelId modelId = (*vi)->getModel();
        if (!modelId.isNone()) {
            if (dynamic_cast<WaveformLayer *>(*vi)) {
                waveformModelId = modelId;
                workModelId = modelId;
            } else {
                if (ModelById::isa<WaveFileModel>(modelId)) {
                    workModelId = modelId;
                } else {
                    ModelId sourceId = (*vi)->getSourceModel();
                    if (ModelById::isa<WaveFileModel>(sourceId)) {
                        workModelId = sourceId;
                    }
                }
            }
        }
                
        if (!waveformModelId.isNone() &&
            !workModelId.isNone() &&
            haveSomeTimeXAxis) {
            break;
        }
    }

    // Block off left and right extents so we can see where the main model ends
    
    if (!workModelId.isNone() && hasTopLayerTimeXAxis()) {
        drawModelTimeExtents(r, paint, workModelId);
    }

    // Crosshairs for mouse movement in measure mode
    
    if (m_manager &&
        m_mouseInWidget &&
        toolMode == ViewManager::MeasureMode) {

        for (LayerList::iterator vi = m_layerStack.end(); vi != m_layerStack.begin(); ) {
            --vi;

            std::vector<QRect> crosshairExtents;

            if ((*vi)->getCrosshairExtents(this, paint, m_identifyPoint,
                                           crosshairExtents)) {
                (*vi)->paintCrosshairs(this, paint, m_identifyPoint);
                break;
            } else if ((*vi)->isLayerOpaque()) {
                break;
            }
        }
    }

    // Scale width will be set implicitly during drawVerticalScale call
    m_scaleWidth = 0;

    if (m_manager && m_manager->shouldShowVerticalScale() && topLayer) {
        drawVerticalScale(r, topLayer, paint);
    }

    // Feature description: the box in top-right showing values from
    // the nearest feature to the mouse
    
    if (m_identifyFeatures &&
        m_manager && m_manager->shouldIlluminateLocalFeatures() &&
        topLayer) {
        drawFeatureDescription(topLayer, paint);
    }
    
    sv_samplerate_t sampleRate = getModelsSampleRate();
    paint.setBrush(Qt::NoBrush);

    if (m_centreLineVisible &&
        m_manager &&
        m_manager->shouldShowCentreLine()) {
        drawCentreLine(sampleRate, paint, !haveSomeTimeXAxis);
    }
    
    paint.setPen(QColor(50, 50, 50));

    if (!waveformModelId.isNone() &&
        sampleRate &&
        m_manager &&
        m_manager->shouldShowDuration()) {
        drawDurationAndRate(r, waveformModelId, sampleRate, paint);
    }

    bool haveWorkTitle = false;

    if (!workModelId.isNone() &&
        m_manager &&
        m_manager->shouldShowWorkTitle()) {
        drawWorkTitle(r, paint, workModelId);
        haveWorkTitle = true;
    }

    if (!workModelId.isNone() &&
        m_manager &&
        m_manager->getAlignMode()) {
        drawAlignmentStatus(r, paint, workModelId, haveWorkTitle);
    }

    if (m_manager &&
        m_manager->shouldShowLayerNames()) {
        drawLayerNames(r, paint);
    }

    // The blue box that is shown when you ctrl-click in navigate mode
    // to define a zoom region
    
    if (m_shiftPressed && m_clickedInRange &&
        (toolMode == ViewManager::NavigateMode || m_navigating)) {

        //!!! be nice if this looked a bit more in keeping with the
        //selection block
        
        paint.setPen(Qt::blue);
        //!!! shouldn't use clickPos -- needs to use a clicked frame
        paint.drawRect(m_clickPos.x(), m_clickPos.y(),
                       m_mousePos.x() - m_clickPos.x(),
                       m_mousePos.y() - m_clickPos.y());

    }

    if (toolMode == ViewManager::MeasureMode && topLayer) {
        bool showFocus = false;
        if (!m_manager || !m_manager->isPlaying()) showFocus = true;
        topLayer->paintMeasurementRects(this, paint, showFocus, m_identifyPoint);
    }
    
    if (selectionIsBeingEdited()) {
        drawEditingSelection(paint);
    }

    paint.end();
}

int
Pane::getVerticalScaleWidth() const
{
    if (m_scaleWidth > 0) return m_scaleWidth;
    else return 0;
}

void
Pane::drawVerticalScale(QRect r, Layer *topLayer, QPainter &paint)
{
    Layer *scaleLayer = nullptr;

//    cerr << "Pane::drawVerticalScale[" << this << "]" << endl;
    
    double min, max;
    bool log;
    QString unit;

    // If the top layer has no scale and reports no display extents,
    // but does report a unit, then the scale should be drawn from any
    // (visible) underlying layer with a scale and that unit.  If the
    // top layer has no scale and no value extents at all, then the
    // scale should be drawn from any (visible) underlying layer with
    // a scale regardless of unit.

    int sw = topLayer->getVerticalScaleWidth
        (this, m_manager->shouldShowVerticalColourScale(), paint);
    
    if (sw > 0) {
        scaleLayer = topLayer;
        m_scaleWidth = sw;

    } else {

        bool hasDisplayExtents = topLayer->getDisplayExtents(min, max);
        bool hasValueExtents = topLayer->getValueExtents(min, max, log, unit);
            
        if (!hasDisplayExtents) {

            if (!hasValueExtents) {

                for (LayerList::iterator vi = m_layerStack.end();
                     vi != m_layerStack.begin(); ) {
                        
                    --vi;
                        
                    if ((*vi) == topLayer) continue;
                    if ((*vi)->isLayerDormant(this)) continue;
                        
                    sw = (*vi)->getVerticalScaleWidth
                        (this, m_manager->shouldShowVerticalColourScale(), paint);
                        
                    if (sw > 0) {
                        scaleLayer = *vi;
                        m_scaleWidth = sw;
                        break;
                    }
                }
            } else if (unit != "") { // && hasValueExtents && !hasDisplayExtents

                QString requireUnit = unit;

                for (LayerList::iterator vi = m_layerStack.end();
                     vi != m_layerStack.begin(); ) {
                        
                    --vi;
                        
                    if ((*vi) == topLayer) continue;
                    if ((*vi)->isLayerDormant(this)) continue;
                        
                    if ((*vi)->getDisplayExtents(min, max)) {
                            
                        // search no further than this: if the
                        // scale from this layer isn't suitable,
                        // we'll have to draw no scale (else we'd
                        // risk ending up with the wrong scale)
                            
                        if ((*vi)->getValueExtents(min, max, log, unit) &&
                            unit == requireUnit) {

                            sw = (*vi)->getVerticalScaleWidth
                                (this, m_manager->shouldShowVerticalColourScale(), paint);
                            if (sw > 0) {
                                scaleLayer = *vi;
                                m_scaleWidth = sw;
                            }
                        }
                        break;
                    }
                }
            }
        }
    }

    if (!scaleLayer) m_scaleWidth = 0;

//    cerr << "m_scaleWidth = " << m_scaleWidth << ", r.left = " << r.left() << endl;
    
    if (m_scaleWidth > 0 && r.left() < m_scaleWidth) {

//      Profiler profiler("Pane::paintEvent - painting vertical scale", true);

//      SVDEBUG << "Pane::paintEvent: calling paint.save() in vertical scale block" << endl;
        paint.save();
            
        paint.setPen(Qt::NoPen);
        paint.setBrush(getBackground());
        paint.drawRect(0, 0, m_scaleWidth, height());
        
        paint.setPen(getForeground());
        paint.drawLine(m_scaleWidth, 0, m_scaleWidth, height());

        paint.setBrush(Qt::NoBrush);
        scaleLayer->paintVerticalScale
            (this, m_manager->shouldShowVerticalColourScale(),
             paint, QRect(0, 0, m_scaleWidth, height()));
        
        paint.restore();
    }
}
            
void
Pane::drawFeatureDescription(Layer *topLayer, QPainter &paint)
{
    QPoint pos = m_identifyPoint;
    QString desc = topLayer->getFeatureDescription(this, pos);
        
    if (desc != "") {
        
        paint.save();
        
    // Qt 5.13 deprecates QFontMetrics::width(), but its suggested
    // replacement (horizontalAdvance) was only added in Qt 5.11
    // which is too new for us
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

        int tabStop =
            paint.fontMetrics().width(tr("Some lengthy prefix:"));
        
        QRect boundingRect = 
            paint.fontMetrics().boundingRect
            (rect(),
             Qt::AlignRight | Qt::AlignTop | Qt::TextExpandTabs,
             desc, tabStop);
        
        if (hasLightBackground()) {
            paint.setPen(Qt::NoPen);
            paint.setBrush(QColor(250, 250, 250, 200));
        } else {
            paint.setPen(Qt::NoPen);
            paint.setBrush(QColor(50, 50, 50, 200));
        }
        
        int extra = paint.fontMetrics().descent();
        paint.drawRect(width() - boundingRect.width() - 10 - extra,
                       10 - extra,
                       boundingRect.width() + 2 * extra,
                       boundingRect.height() + extra);
        
        if (hasLightBackground()) {
            paint.setPen(QColor(150, 20, 0));
        } else {
            paint.setPen(QColor(255, 150, 100));
        }
        
        QTextOption option;
        option.setWrapMode(QTextOption::NoWrap);
        option.setAlignment(Qt::AlignRight | Qt::AlignTop);
        option.setTabStop(tabStop);
        paint.drawText(QRectF(width() - boundingRect.width() - 10, 10,
                              boundingRect.width(),
                              boundingRect.height()),
                       desc,
                       option);
        
        paint.restore();
    }
}

void
Pane::drawCentreLine(sv_samplerate_t sampleRate, QPainter &paint, bool omitLine)
{
    if (omitLine && m_manager->getMainModelSampleRate() == 0) {
        return;
    }
    
    int fontHeight = paint.fontMetrics().height();
    int fontAscent = paint.fontMetrics().ascent();

    QColor c = QColor(0, 0, 0);
    if (!hasLightBackground()) {
        c = QColor(240, 240, 240);
    }

    paint.setPen(scalePen(c));
    int x = width() / 2;

    if (!omitLine) {
        paint.drawLine(x, 0, x, height() - 1);
        paint.drawLine(x-1, 1, x+1, 1);
        paint.drawLine(x-2, 0, x+2, 0);
        paint.drawLine(x-1, height() - 2, x+1, height() - 2);
        paint.drawLine(x-2, height() - 1, x+2, height() - 1);
    }
    
    paint.setPen(QColor(50, 50, 50));
    
    int y = height() - fontHeight + fontAscent - 6;
    
    LayerList::iterator vi = m_layerStack.end();
    
    if (vi != m_layerStack.begin()) {
        
        switch ((*--vi)->getPreferredFrameCountPosition()) {
            
        case Layer::PositionTop:
            y = fontAscent + 6;
            break;
            
        case Layer::PositionMiddle:
            y = (height() - fontHeight) / 2
                + fontAscent;
            break;
            
        case Layer::PositionBottom:
            // y already set correctly
            break;
        }
    }
    
    if (m_manager && m_manager->shouldShowFrameCount()) {
        
        if (sampleRate) {

            QString text(QString::fromStdString
                         (RealTime::frame2RealTime
                          (m_centreFrame, sampleRate)
                          .toText(true)));
            
            int tw = paint.fontMetrics().width(text);
            int x = width()/2 - 4 - tw;
            
            PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
        }
        
        QString text = QString("%1").arg(m_centreFrame);
        
        int x = width()/2 + 4;
        
        PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
    }
}

void
Pane::drawModelTimeExtents(QRect r, QPainter &paint, ModelId modelId)
{
    auto model = ModelById::get(modelId);
    if (!model) return;

    paint.save();
    
    QBrush brush;

    if (hasLightBackground()) {
        brush = QBrush(QColor("#aaf8f8f8"));
        paint.setPen(Qt::black);
    } else {
        brush = QBrush(QColor("#aa101010"));
        paint.setPen(Qt::white);
    }

    sv_frame_t f0 = model->getStartFrame();

    if (f0 > getStartFrame() && f0 < getEndFrame()) {
        int x0 = getXForFrame(f0);
        if (x0 > r.x()) {
            paint.fillRect(0, 0, x0, height(), brush);
            paint.drawLine(x0, 0, x0, height());
        }
    }

    sv_frame_t f1 = model->getEndFrame();
    
    if (f1 > getStartFrame() && f1 < getEndFrame()) {
        int x1 = getXForFrame(f1);
        if (x1 < r.x() + r.width()) {
            paint.fillRect(x1, 0, width() - x1, height(), brush);
            paint.drawLine(x1, 0, x1, height());
        }
    }

    paint.restore();
}

void
Pane::drawAlignmentStatus(QRect r, QPainter &paint, ModelId modelId,
                          bool down)
{
    auto model = ModelById::get(modelId);
    if (!model) return;
    
    ModelId reference = model->getAlignmentReference();
/*
    if (!reference) {
        cerr << "Pane[" << this << "]::drawAlignmentStatus: No reference" << endl;
    } else if (reference == model->getId()) {
        cerr << "Pane[" << this << "]::drawAlignmentStatus: This is the reference model" << endl;
    } else {
        cerr << "Pane[" << this << "]::drawAlignmentStatus: This is not the reference" << endl;
    }
*/
    QString text;
    int completion = 100;

    if (reference == modelId) {
        text = tr("Reference");
    } else if (reference.isNone()) {
        text = tr("Unaligned");
    } else {
        completion = model->getAlignmentCompletion();
        if (completion == 0) {
            text = tr("Unaligned");
        } else if (completion < 100) {
            text = tr("Aligning: %1%").arg(completion);
        } else {
            text = tr("Aligned");
        }
    }

    paint.save();
    QFont font(paint.font());
    font.setBold(true);
    paint.setFont(font);
    if (completion < 100) paint.setBrush(Qt::red);

    int y = 5;
    if (down) y += paint.fontMetrics().height();
    int w = paint.fontMetrics().width(text);
    int h = paint.fontMetrics().height();
    if (r.top() > h + y || r.left() > w + m_scaleWidth + 5) {
        paint.restore();
        return;
    }
    
    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);

    paint.restore();
}

void
Pane::modelAlignmentCompletionChanged(ModelId modelId)
{
    View::modelAlignmentCompletionChanged(modelId);
    update(QRect(0, 0, 300, 100));
}

void
Pane::drawWorkTitle(QRect r, QPainter &paint, ModelId modelId)
{
    auto model = ModelById::get(modelId);
    if (!model) return;
    
    QString title = model->getTitle();
    QString maker = model->getMaker();
//SVDEBUG << "Pane::drawWorkTitle: title=\"" << title//<< "\", maker=\"" << maker << "\"" << endl;
    if (title == "") return;

    QString text = title;
    if (maker != "") {
        text = tr("%1 - %2").arg(title).arg(maker);
    }
    
    paint.save();
    QFont font(paint.font());
    font.setItalic(true);
    paint.setFont(font);

    int y = 5;
    int w = paint.fontMetrics().width(text);
    int h = paint.fontMetrics().height();
    if (r.top() > h + y || r.left() > w + m_scaleWidth + 5) {
        paint.restore();
        return;
    }
    
    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);

    paint.restore();
}

void
Pane::drawLayerNames(QRect r, QPainter &paint)
{
    int fontHeight = paint.fontMetrics().height();
    int fontAscent = paint.fontMetrics().ascent();

    int lly = height() - 6;

    int zoomWheelSkip = 0, horizontalScaleSkip = 0;

    if (m_manager->getZoomWheelsEnabled()) {
        zoomWheelSkip = m_manager->scalePixelSize(20);
    }

    for (LayerList::iterator i = m_layerStack.end(); i != m_layerStack.begin();) {
        --i;
        horizontalScaleSkip = (*i)->getHorizontalScaleHeight(this, paint);
        if (horizontalScaleSkip > 0) {
            break;
        }
        if ((*i)->isLayerOpaque()) {
            break;
        }
    }

    lly -= std::max(zoomWheelSkip, horizontalScaleSkip);
    
    if (r.y() + r.height() < lly - int(m_layerStack.size()) * fontHeight) {
        return;
    }

    QStringList texts;
    std::vector<QPixmap> pixmaps;
    for (LayerList::iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
        texts.push_back((*i)->getLayerPresentationName());
//        cerr << "Pane " << this << ": Layer presentation name for " << *i << ": "
//                  << texts[texts.size()-1] << endl;
        pixmaps.push_back((*i)->getLayerPresentationPixmap
                          (QSize(fontAscent, fontAscent)));
    }

    int maxTextWidth = width() / 3;
    texts = TextAbbrev::abbreviate(texts, paint.fontMetrics(), maxTextWidth);

    int llx = width() - maxTextWidth - 5;
    if (m_manager->getZoomWheelsEnabled()) {
        llx -= m_manager->scalePixelSize(36);
    }
    
    if (r.x() + r.width() >= llx - fontAscent - 3) {
    
        for (int i = 0; i < texts.size(); ++i) {

//            cerr << "Pane "<< this << ": text " << i << ": " << texts[i] << endl;
            
            if (i + 1 == texts.size()) {
                paint.setPen(getForeground());
            }
            
            PaintAssistant::drawVisibleText(this, paint, llx,
                            lly - fontHeight + fontAscent,
                            texts[i], PaintAssistant::OutlinedText);

            if (!pixmaps[i].isNull()) {
                paint.drawPixmap(llx - fontAscent - 3,
                                 lly - fontHeight + (fontHeight-fontAscent)/2,
                                 pixmaps[i]);
            }
            
            lly -= fontHeight;
        }
    }
}

void
Pane::drawEditingSelection(QPainter &paint)
{
    int offset = m_mousePos.x() - m_clickPos.x();

    sv_frame_t origStart = m_editingSelection.getStartFrame();

    int p0 = getXForFrame(origStart) + offset;
    int p1 = getXForFrame(m_editingSelection.getEndFrame()) + offset;

    if (m_editingSelectionEdge < 0) {
        p1 = getXForFrame(m_editingSelection.getEndFrame());
    } else if (m_editingSelectionEdge > 0) {
        p0 = getXForFrame(m_editingSelection.getStartFrame());
    }
    
    sv_frame_t newStart = getFrameForX(p0);
    sv_frame_t newEnd = getFrameForX(p1);
    
    paint.save();
    paint.setPen(QPen(getForeground(), 2));

    int fontHeight = paint.fontMetrics().height();
    int fontAscent = paint.fontMetrics().ascent();
    sv_samplerate_t sampleRate = getModelsSampleRate();
    QString startText, endText, offsetText;
    startText = QString("%1").arg(newStart);
    endText = QString("%1").arg(newEnd);
    offsetText = QString("%1").arg(newStart - origStart);
    if (newStart >= origStart) {
        offsetText = tr("+%1").arg(offsetText);
    }
    if (sampleRate) {
        startText = QString("%1 / %2")
            .arg(QString::fromStdString
                 (RealTime::frame2RealTime(newStart, sampleRate).toText()))
            .arg(startText);
        endText = QString("%1 / %2")
            .arg(QString::fromStdString
                 (RealTime::frame2RealTime(newEnd, sampleRate).toText()))
            .arg(endText);
        offsetText = QString("%1 / %2")
            .arg(QString::fromStdString
                 (RealTime::frame2RealTime(newStart - origStart, sampleRate).toText()))
            .arg(offsetText);
        if (newStart >= origStart) {
            offsetText = tr("+%1").arg(offsetText);
        }
    }
    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight + 4, startText, PaintAssistant::OutlinedText);
    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight + 4, endText, PaintAssistant::OutlinedText);
    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
    
    //!!! duplicating display policy with View::drawSelections
    
    if (m_editingSelectionEdge < 0) {
        paint.drawLine(p0, 1, p1, 1);
        paint.drawLine(p0, 0, p0, height());
        paint.drawLine(p0, height() - 1, p1, height() - 1);
    } else if (m_editingSelectionEdge > 0) {
        paint.drawLine(p0, 1, p1, 1);
        paint.drawLine(p1, 0, p1, height());
        paint.drawLine(p0, height() - 1, p1, height() - 1);
    } else {
        paint.setBrush(Qt::NoBrush);
        paint.drawRect(p0, 1, p1 - p0, height() - 2);
    }
    paint.restore();
}

void
Pane::drawDurationAndRate(QRect r, ModelId waveformModelId,
                          sv_samplerate_t sampleRate, QPainter &paint)
{
    auto waveformModel = ModelById::get(waveformModelId);
    if (!waveformModel) return;
    
    int fontHeight = paint.fontMetrics().height();
    int fontAscent = paint.fontMetrics().ascent();

    if (r.y() + r.height() < height() - fontHeight - 6) return;

    sv_samplerate_t modelRate = waveformModel->getSampleRate();
    sv_samplerate_t nativeRate = waveformModel->getNativeRate();
    sv_samplerate_t playbackRate = m_manager->getPlaybackSampleRate();
        
    QString srNote = "";

    // Show (R) for waveform models that have been resampled during
    // load, and (X) for waveform models that will be played at the
    // wrong rate because their rate differs from the current playback
    // rate (which is not necessarily that of the main model).

    if (modelRate != nativeRate) {
        if (playbackRate != 0 && modelRate != playbackRate) {
            srNote = " " + tr("(X)");
        } else {            
            srNote = " " + tr("(R)");
        }
    }

    QString desc = tr("%1 / %2Hz%3")
        .arg(RealTime::frame2RealTime(waveformModel->getEndFrame(),
                                      sampleRate)
             .toText(false).c_str())
        .arg(nativeRate)
        .arg(srNote);

    int x = m_scaleWidth + 5;
    int pbw = getProgressBarWidth();
    if (x < pbw + 5) x = pbw + 5;

    if (r.x() < x + paint.fontMetrics().width(desc)) {
        PaintAssistant::drawVisibleText(this, paint, x,
                        height() - fontHeight + fontAscent - 6,
                        desc, PaintAssistant::OutlinedText);
    }
}

bool
Pane::render(QPainter &paint, int xorigin, sv_frame_t f0, sv_frame_t f1)
{
    if (!View::render(paint, xorigin + m_scaleWidth, f0, f1)) {
        return false;
    }

    if (m_scaleWidth > 0) {

        Layer *layer = getTopLayer();

        if (layer) {
            
            paint.save();
            
            paint.setPen(getForeground());
            paint.setBrush(getBackground());
            paint.drawRect(xorigin, -1, m_scaleWidth, height()+1);
            
            paint.setBrush(Qt::NoBrush);
            layer->paintVerticalScale
                (this, m_manager->shouldShowVerticalColourScale(),
                 paint, QRect(xorigin, 0, m_scaleWidth, height()));
            
            paint.restore();
        }
    }

    return true;
}

QImage *
Pane::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
{
    int x0 = int(round(getZoomLevel().framesToPixels(double(f0))));
    int x1 = int(round(getZoomLevel().framesToPixels(double(f1))));

    QImage *image = new QImage(x1 - x0 + m_scaleWidth,
                               height(), QImage::Format_RGB32);

    int formerScaleWidth = m_scaleWidth;
            
    if (m_manager && m_manager->shouldShowVerticalScale()) {
        Layer *layer = getTopLayer();
        if (layer) {
            QPainter paint(image);
            m_scaleWidth = layer->getVerticalScaleWidth
                (this, m_manager->shouldShowVerticalColourScale(), paint);
        }
    } else {
        m_scaleWidth = 0;
    }

    if (m_scaleWidth != formerScaleWidth) {
        delete image;
        image = new QImage(x1 - x0 + m_scaleWidth,
                           height(), QImage::Format_RGB32);
    }        

    QPainter *paint = new QPainter(image);
    if (!render(*paint, 0, f0, f1)) {
        delete paint;
        delete image;
        return nullptr;
    } else {
        delete paint;
        return image;
    }
}

QSize
Pane::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
{
    QSize s = View::getRenderedPartImageSize(f0, f1);
    QImage *image = new QImage(100, 100, QImage::Format_RGB32);
    QPainter paint(image);

    int sw = 0;
    if (m_manager && m_manager->shouldShowVerticalScale()) {
        Layer *layer = getTopLayer();
        if (layer) {
            sw = layer->getVerticalScaleWidth
                (this, m_manager->shouldShowVerticalColourScale(), paint);
        }
    }
    
    return QSize(sw + s.width(), s.height());
}

sv_frame_t
Pane::getFirstVisibleFrame() const
{
    sv_frame_t f0 = getFrameForX(m_scaleWidth);
    sv_frame_t f = View::getFirstVisibleFrame();
    if (f0 < 0 || f0 < f) return f;
    return f0;
}

Selection
Pane::getSelectionAt(int x, bool &closeToLeftEdge, bool &closeToRightEdge) const
{
    closeToLeftEdge = closeToRightEdge = false;

    if (!m_manager) return Selection();

    sv_frame_t testFrame = getFrameForX(x - scalePixelSize(5));
    if (testFrame < 0) {
        testFrame = getFrameForX(x);
        if (testFrame < 0) return Selection();
    }

    Selection selection = m_manager->getContainingSelection(testFrame, true);
    if (selection.isEmpty()) return selection;

    int lx = getXForFrame(selection.getStartFrame());
    int rx = getXForFrame(selection.getEndFrame());
    
    int fuzz = scalePixelSize(2);
    if (x < lx - fuzz || x > rx + fuzz) return Selection();

    int width = rx - lx;
    fuzz = scalePixelSize(3);
    if (width < 12) fuzz = width / 4;
    if (fuzz < scalePixelSize(1)) {
        fuzz = scalePixelSize(1);
    }

    if (x < lx + fuzz) closeToLeftEdge = true;
    if (x > rx - fuzz) closeToRightEdge = true;

    return selection;
}

bool
Pane::canTopLayerMoveVertical()
{
    double vmin, vmax, dmin, dmax;
    if (!getTopLayerDisplayExtents(vmin, vmax, dmin, dmax)) return false;
    if (dmin <= vmin && dmax >= vmax) return false;
    return true;
}

bool
Pane::getTopLayerDisplayExtents(double &vmin, double &vmax,
                                double &dmin, double &dmax,
                                QString *unit) 
{
    Layer *layer = getTopLayer();
    if (!layer) return false;
    bool vlog;
    QString vunit;
    bool rv = (layer->getValueExtents(vmin, vmax, vlog, vunit) &&
               layer->getDisplayExtents(dmin, dmax));
    if (unit) *unit = vunit;
    return rv;
}

bool
Pane::setTopLayerDisplayExtents(double dmin, double dmax)
{
    Layer *layer = getTopLayer();
    if (!layer) return false;
    return layer->setDisplayExtents(dmin, dmax);
}

void
Pane::registerShortcuts(KeyReference &kr)
{
    kr.setCategory(tr("Zoom"));
    kr.registerAlternativeShortcut(tr("Zoom In"), tr("Wheel Up"));
    kr.registerAlternativeShortcut(tr("Zoom Out"), tr("Wheel Down"));

    kr.setCategory(tr("General Pane Mouse Actions"));
    
    kr.registerShortcut(tr("Zoom"), tr("Wheel"),
                        tr("Zoom in or out in time axis"));
    kr.registerShortcut(tr("Scroll"), tr("Ctrl+Wheel"),
                        tr("Scroll rapidly left or right in time axis"));
    kr.registerShortcut(tr("Zoom Vertically"), tr("Shift+Wheel"), 
                        tr("Zoom in or out in the vertical axis"));
    kr.registerShortcut(tr("Scroll Vertically"), tr("Alt+Wheel"), 
                        tr("Scroll up or down in the vertical axis"));
    kr.registerShortcut(tr("Navigate"), tr("Middle"), 
                        tr("Click middle button and drag to navigate with any tool"));
    kr.registerShortcut(tr("Relocate"), tr("Double-Click Middle"), 
                        tr("Double-click middle button to relocate with any tool"));
    kr.registerShortcut(tr("Menu"), tr("Right"),
                        tr("Show pane context menu"));
}

Layer *
Pane::getTopFlexiNoteLayer()
{
    for (int i = int(m_layerStack.size()) - 1; i >= 0; --i) {
        if (LayerFactory::getInstance()->getLayerType(m_layerStack[i]) ==
            LayerFactory::FlexiNotes) {
            return m_layerStack[i];
        }
    }
    return nullptr;
}

void
Pane::mousePressEvent(QMouseEvent *e)
{
    if (e->buttons() & Qt::RightButton) {
        emit contextHelpChanged("");
        emit rightButtonMenuRequested(mapToGlobal(e->pos()));
        return;
    }

//    cerr << "mousePressEvent" << endl;

    m_clickPos = e->pos();
    m_mousePos = m_clickPos;
    m_clickedInRange = true;
    m_editingSelection = Selection();
    m_editingSelectionEdge = 0;
    m_shiftPressed = (e->modifiers() & Qt::ShiftModifier);
    m_ctrlPressed = (e->modifiers() & Qt::ControlModifier);
    m_altPressed = (e->modifiers() & Qt::AltModifier);
    m_dragMode = UnresolvedDrag;

    ViewManager::ToolMode mode = ViewManager::NavigateMode;
    if (m_manager) mode = m_manager->getToolModeFor(this);

    m_navigating = false;
    m_resizing = false;
    m_editing = false;
    m_releasing = false;

    if (mode == ViewManager::NavigateMode ||
        (e->buttons() & Qt::MidButton) ||
        (mode == ViewManager::MeasureMode &&
         (e->buttons() & Qt::LeftButton) && m_shiftPressed)) {

        if (mode != ViewManager::NavigateMode) {
            setCursor(Qt::PointingHandCursor);
        }

        m_navigating = true;
        m_dragCentreFrame = m_centreFrame;
        m_dragStartMinValue = 0;
        
        double vmin, vmax, dmin, dmax;
        if (getTopLayerDisplayExtents(vmin, vmax, dmin, dmax)) {
            m_dragStartMinValue = dmin;
        }

        if (m_followPlay == PlaybackScrollPage) {
            // Schedule a play-head move to the mouse frame
            // location. This will happen only if nothing else of
            // interest happens (double-click, drag) before the
            // timeout.
            schedulePlaybackFrameMove(getFrameForX(e->x()));
        }

    } else if (mode == ViewManager::SelectMode) {

        if (!hasTopLayerTimeXAxis()) return;

        bool closeToLeft = false, closeToRight = false;
        Selection selection = getSelectionAt(e->x(), closeToLeft, closeToRight);

        if ((closeToLeft || closeToRight) && !(closeToLeft && closeToRight)) {

            m_manager->removeSelection(selection);

            if (closeToLeft) {
                m_selectionStartFrame = selection.getEndFrame();
            } else {
                m_selectionStartFrame = selection.getStartFrame();
            }
            
            m_manager->setInProgressSelection(selection, false);
            m_resizing = true;
            
        } else {
            
            sv_frame_t mouseFrame = getFrameForX(e->x());
            int resolution = 1;
            sv_frame_t snapFrame = mouseFrame;
    
            Layer *layer = getInteractionLayer();
            if (layer && !m_shiftPressed &&
                !qobject_cast<TimeRulerLayer *>(layer)) { // don't snap to secs
                layer->snapToFeatureFrame(this, snapFrame,
                                          resolution, Layer::SnapLeft);
            }
        
            if (snapFrame < 0) snapFrame = 0;
            m_selectionStartFrame = snapFrame;
            if (m_manager) {
                m_manager->setInProgressSelection
                    (Selection(alignToReference(snapFrame),
                               alignToReference(snapFrame + resolution)),
                     !m_ctrlPressed);
            }

            m_resizing = false;

            if (m_followPlay == PlaybackScrollPage) {
                // Schedule a play-head move to the mouse frame
                // location. This will happen only if nothing else of
                // interest happens (double-click, drag) before the
                // timeout.
                schedulePlaybackFrameMove(mouseFrame);
            }
        }

        update();

    } else if (mode == ViewManager::DrawMode) {

        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            layer->drawStart(this, e);
        }

    } else if (mode == ViewManager::EraseMode) {

        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            layer->eraseStart(this, e);
        }

        // GF: handle mouse press for NoteEditMode 
    } else if (mode == ViewManager::NoteEditMode) {

        std::cerr << "mouse pressed in note edit mode" << std::endl;
        Layer *layer = getTopFlexiNoteLayer();
        if (layer) {
            layer->splitStart(this, e); 
        }

    } else if (mode == ViewManager::EditMode) {

        // Do nothing here -- we'll do it in mouseMoveEvent when the
        // drag threshold has been passed

    } else if (mode == ViewManager::MeasureMode) {

        Layer *layer = getTopLayer();
        if (layer) layer->measureStart(this, e);
        update();
    }

    emit paneInteractedWith();
}

void
Pane::schedulePlaybackFrameMove(sv_frame_t frame)
{
    m_playbackFrameMoveTo = frame;
    m_playbackFrameMoveScheduled = true;
    QTimer::singleShot(QApplication::doubleClickInterval() + 10, this,
                       SLOT(playbackScheduleTimerElapsed()));
}

void
Pane::playbackScheduleTimerElapsed()
{
    if (m_playbackFrameMoveScheduled) {
        m_manager->setPlaybackFrame(m_playbackFrameMoveTo);
        m_playbackFrameMoveScheduled = false;
    }
}

void
Pane::mouseReleaseEvent(QMouseEvent *e)
{
    if (e && (e->buttons() & Qt::RightButton)) {
        return;
    }

//    cerr << "mouseReleaseEvent" << endl;

    ViewManager::ToolMode mode = ViewManager::NavigateMode;
    if (m_manager) mode = m_manager->getToolModeFor(this);

    m_releasing = true;

    if (m_clickedInRange) {
        mouseMoveEvent(e);
    }

    sv_frame_t mouseFrame = e ? getFrameForX(e->x()) : 0;
    if (mouseFrame < 0) mouseFrame = 0;

    if (m_navigating || mode == ViewManager::NavigateMode) {

        m_navigating = false;

        if (mode != ViewManager::NavigateMode) {
            // restore cursor
            toolModeChanged();
        }

        if (m_shiftPressed) {

            int x0 = std::min(m_clickPos.x(), m_mousePos.x());
            int x1 = std::max(m_clickPos.x(), m_mousePos.x());

            int y0 = std::min(m_clickPos.y(), m_mousePos.y());
            int y1 = std::max(m_clickPos.y(), m_mousePos.y());

            emit regionOutlined(QRect(x0, y0, x1 - x0, y1 - y0));
        }

    } else if (mode == ViewManager::SelectMode) {

        if (!hasTopLayerTimeXAxis()) {
            m_releasing = false;
            return;
        }

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

            //cerr << "JTEST: release with selection" << endl;
            bool exclusive;
            Selection selection = m_manager->getInProgressSelection(exclusive);
        
            if (selection.getEndFrame() < selection.getStartFrame() + 2) {
                selection = Selection();
            }
        
            m_manager->clearInProgressSelection();
        
            if (exclusive) {
                m_manager->setSelection(selection);
            } else {
                m_manager->addSelection(selection);
            }
        }
    
        update();

    } else if (mode == ViewManager::DrawMode) {

        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            layer->drawEnd(this, e);
            update();
        }

    } else if (mode == ViewManager::EraseMode) {

        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            layer->eraseEnd(this, e);
            update();
        }

    } else if (mode == ViewManager::NoteEditMode) {
    
        //GF: handle mouse release for NoteEditMode (note: works but will need to re-think this a bit later)
        Layer *layer = getTopFlexiNoteLayer();

        if (layer) {
            layer->splitEnd(this, e);
            update();

            if (m_editing) {
                if (!editSelectionEnd(e)) {
                    layer->editEnd(this, e);
                    update();
                }
            }
        } 

    } else if (mode == ViewManager::EditMode) {
        
        if (m_editing) {
            if (!editSelectionEnd(e)) {
                Layer *layer = getInteractionLayer();
                if (layer && layer->isLayerEditable()) {
                    layer->editEnd(this, e);
                    update();
                }
            }
        } 

    } else if (mode == ViewManager::MeasureMode) {

        Layer *layer = getTopLayer();
        if (layer) layer->measureEnd(this, e);
        if (m_measureCursor1) setCursor(*m_measureCursor1);
        update();
    }

    m_clickedInRange = false;
    m_releasing = false;

    emit paneInteractedWith();
}

void
Pane::mouseMoveEvent(QMouseEvent *e)
{
    if (!e || (e->buttons() & Qt::RightButton)) {
        return;
    }

//    cerr << "mouseMoveEvent" << endl;

    QPoint pos = e->pos();
    updateContextHelp(&pos);

    if (m_navigating && m_clickedInRange && !m_releasing) {

        // if no buttons pressed, and not called from
        // mouseReleaseEvent, we want to reset clicked-ness (to avoid
        // annoying continual drags when we moved the mouse outside
        // the window after pressing button first time).

        if (!(e->buttons() & Qt::LeftButton) &&
            !(e->buttons() & Qt::MidButton)) {
            m_clickedInRange = false;
            return;
        }
    }

    ViewManager::ToolMode mode = ViewManager::NavigateMode;
    if (m_manager) mode = m_manager->getToolModeFor(this);

    QPoint prevPoint = m_identifyPoint;
    m_identifyPoint = e->pos();

    if (!m_clickedInRange) {
    
        // GF: handle mouse move for context sensitive cursor switching in NoteEditMode.
        // GF: Propagate the event to FlexiNoteLayer. I somehow feel it's best handeled there rather than here, but perhaps not if this will be needed elsewhere too.
        if (mode == ViewManager::NoteEditMode) {
            FlexiNoteLayer *layer = qobject_cast<FlexiNoteLayer *>(getTopFlexiNoteLayer());
            if (layer) {
                layer->mouseMoveEvent(this, e); //!!! ew
                update();
                // return;
            }
        }   
    
        if (mode == ViewManager::SelectMode && hasTopLayerTimeXAxis()) {
            bool closeToLeft = false, closeToRight = false;
            getSelectionAt(e->x(), closeToLeft, closeToRight);
            if ((closeToLeft || closeToRight) && !(closeToLeft && closeToRight)) {
                setCursor(Qt::SizeHorCursor);
            } else {
                setCursor(Qt::ArrowCursor);
            }
        }

        if (m_manager && !m_manager->isPlaying()) {

            bool updating = false;

            if (getInteractionLayer() &&
                m_manager->shouldIlluminateLocalFeatures()) {

                bool previouslyIdentifying = m_identifyFeatures;
                m_identifyFeatures = true;
                
                if (m_identifyFeatures != previouslyIdentifying ||
                    m_identifyPoint != prevPoint) {
                    update();
                    updating = true;
                }
            }

            if (!updating && mode == ViewManager::MeasureMode) {

                Layer *layer = getTopLayer();
                if (layer && layer->nearestMeasurementRectChanged
                    (this, prevPoint, m_identifyPoint)) {
                    update();
                }
            }
        }

        return;
    }

    if (m_navigating || mode == ViewManager::NavigateMode) {

        if (m_shiftPressed) {

            m_mousePos = e->pos();
            update();

        } else {

            dragTopLayer(e);
        }

    } else if (mode == ViewManager::SelectMode) {

        if (!hasTopLayerTimeXAxis()) return;

        dragExtendSelection(e);

    } else if (mode == ViewManager::DrawMode) {

        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            layer->drawDrag(this, e);
        }

    } else if (mode == ViewManager::EraseMode) {

        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            layer->eraseDrag(this, e);
        }

        // GF: handling NoteEditMode dragging and boundary actions for mouseMoveEvent
    } else if (mode == ViewManager::NoteEditMode) {

        bool resist = true;

        if ((e->modifiers() & Qt::ShiftModifier)) {
            m_shiftPressed = true;
        }

        if (m_shiftPressed) resist = false;

        m_dragMode = updateDragMode
            (m_dragMode,
             m_clickPos,
             e->pos(),
             true,    // can move horiz
             true,    // can move vert
             resist,  // resist horiz
             resist); // resist vert

        if (!m_editing) {

            if (m_dragMode != UnresolvedDrag) {

                m_editing = true;

                QMouseEvent clickEvent(QEvent::MouseButtonPress,
                                       m_clickPos,
                                       Qt::NoButton,
                                       e->buttons(),
                                       e->modifiers());

                if (!editSelectionStart(&clickEvent)) {
                    Layer *layer = getTopFlexiNoteLayer();
                    if (layer) {
                        std::cerr << "calling edit start" << std::endl;
                        layer->editStart(this, &clickEvent);
                    }
                }
            }

        } else {

            if (!editSelectionDrag(e)) {

                Layer *layer = getTopFlexiNoteLayer();

                if (layer) {

                    int x = e->x();
                    int y = e->y();
                    if (m_dragMode == VerticalDrag) x = m_clickPos.x();
                    else if (m_dragMode == HorizontalDrag) y = m_clickPos.y();

                    QMouseEvent moveEvent(QEvent::MouseMove,
                                          QPoint(x, y),
                                          Qt::NoButton,
                                          e->buttons(),
                                          e->modifiers());
                    std::cerr << "calling editDrag" << std::endl;
                    layer->editDrag(this, &moveEvent);
                }
            }
        }

    } else if (mode == ViewManager::EditMode) {

        bool resist = true;

        if ((e->modifiers() & Qt::ShiftModifier)) {
            m_shiftPressed = true;
            // ... but don't set it false if shift has been
            // released -- we want the state when we started
            // dragging to be used most of the time
        }

        if (m_shiftPressed) resist = false;

        m_dragMode = updateDragMode
            (m_dragMode,
             m_clickPos,
             e->pos(),
             true,    // can move horiz
             true,    // can move vert
             resist,  // resist horiz
             resist); // resist vert

        if (!m_editing) {

            if (m_dragMode != UnresolvedDrag) {

                m_editing = true;

                QMouseEvent clickEvent(QEvent::MouseButtonPress,
                                       m_clickPos,
                                       Qt::NoButton,
                                       e->buttons(),
                                       e->modifiers());

                if (!editSelectionStart(&clickEvent)) {
                    Layer *layer = getInteractionLayer();
                    if (layer && layer->isLayerEditable()) {
                        layer->editStart(this, &clickEvent);
                    }
                }
            }

        } else {

            if (!editSelectionDrag(e)) {

                Layer *layer = getInteractionLayer();

                if (layer && layer->isLayerEditable()) {

                    int x = e->x();
                    int y = e->y();
                    if (m_dragMode == VerticalDrag) x = m_clickPos.x();
                    else if (m_dragMode == HorizontalDrag) y = m_clickPos.y();

                    QMouseEvent moveEvent(QEvent::MouseMove,
                                          QPoint(x, y),
                                          Qt::NoButton,
                                          e->buttons(),
                                          e->modifiers());
                                              
                    layer->editDrag(this, &moveEvent);
                }
            }
        }

    } else if (mode == ViewManager::MeasureMode) {

        if (m_measureCursor2) setCursor(*m_measureCursor2);

        Layer *layer = getTopLayer();
        if (layer) {
            layer->measureDrag(this, e);
            if (layer->hasTimeXAxis()) edgeScrollMaybe(e->x());
        }

        update();
    }
    
    if (m_dragMode != UnresolvedDrag) {
        m_playbackFrameMoveScheduled = false;
    }
}

void
Pane::zoomToRegion(QRect r)
{
    int x0 = r.x();
    int y0 = r.y();
    int x1 = r.x() + r.width();
    int y1 = r.y() + r.height();

    SVDEBUG << "Pane::zoomToRegion: region defined by pixel rect ("
            << r.x() << "," << r.y() << "), " << r.width() << "x" << r.height()
            << endl;

    Layer *interactionLayer = getInteractionLayer();
    if (interactionLayer && !(interactionLayer->hasTimeXAxis())) {
        SVDEBUG << "Interaction layer does not have time X axis - delegating to it to decide what to do" << endl;
        interactionLayer->zoomToRegion(this, r);
        return;
    }
    
    sv_frame_t newStartFrame = getFrameForX(x0);
    sv_frame_t newEndFrame = getFrameForX(x1);
    sv_frame_t dist = newEndFrame - newStartFrame;
        
    sv_frame_t visibleFrames = getEndFrame() - getStartFrame();
    if (newStartFrame <= -visibleFrames) {
        newStartFrame  = -visibleFrames + 1;
    }
        
    if (newStartFrame >= getModelsEndFrame()) {
        newStartFrame  = getModelsEndFrame() - 1;
    }

    ZoomLevel newZoomLevel = ZoomLevel::fromRatio(width(), dist);
    setZoomLevel(getZoomConstraintLevel(newZoomLevel));
    setStartFrame(newStartFrame);

    QString unit;
    double min, max;
    bool log;
    Layer *layer = nullptr;
    for (LayerList::const_iterator i = m_layerStack.begin();
         i != m_layerStack.end(); ++i) { 
        if ((*i)->getValueExtents(min, max, log, unit) &&
            (*i)->getDisplayExtents(min, max)) {
            layer = *i;
            break;
        }
    }
            
    if (layer) {
        if (log) {
            min = (min < 0.0) ? -log10(-min) : (min == 0.0) ? 0.0 : log10(min);
            max = (max < 0.0) ? -log10(-max) : (max == 0.0) ? 0.0 : log10(max);
        }
        double rmin = min + ((max - min) * (height() - y1)) / height();
        double rmax = min + ((max - min) * (height() - y0)) / height();
        cerr << "min: " << min << ", max: " << max << ", y0: " << y0 << ", y1: " << y1 << ", h: " << height() << ", rmin: " << rmin << ", rmax: " << rmax << endl;
        if (log) {
            rmin = pow(10, rmin);
            rmax = pow(10, rmax);
        }
        cerr << "finally: rmin: " << rmin << ", rmax: " << rmax << " " << unit << endl;

        layer->setDisplayExtents(rmin, rmax);
        updateVerticalPanner();
    }
}

void
Pane::dragTopLayer(QMouseEvent *e)
{
    // We need to avoid making it too easy to drag both
    // horizontally and vertically, in the case where the
    // mouse is moved "mostly" in horizontal or vertical axis
    // with only a small variation in the other axis.  This is
    // particularly important during playback (when we want to
    // avoid small horizontal motions) or in slow refresh
    // layers like spectrogram (when we want to avoid small
    // vertical motions).
    // 
    // To this end we have horizontal and vertical thresholds
    // and a series of states: unresolved, horizontally or
    // vertically constrained, free.
    //
    // When the mouse first moves, we're unresolved: we
    // restrict ourselves to whichever direction seems safest,
    // until the mouse has passed a small threshold distance
    // from the click point.  Then we lock in to one of the
    // constrained modes, based on which axis that distance
    // was measured in first.  Finally, if it turns out we've
    // also moved more than a certain larger distance in the
    // other direction as well, we may switch into free mode.
    // 
    // If the top layer is incapable of being dragged
    // vertically, the logic is short circuited.

    m_dragMode = updateDragMode
        (m_dragMode,
         m_clickPos,
         e->pos(),
         true, // can move horiz
         canTopLayerMoveVertical(), // can move vert
         canTopLayerMoveVertical() || (m_manager && m_manager->isPlaying()), // resist horiz
         true); // resist vert

    if (m_dragMode == HorizontalDrag ||
        m_dragMode == FreeDrag) {

        sv_frame_t frameOff = getFrameForX(e->x()) - getFrameForX(m_clickPos.x());
        sv_frame_t newCentreFrame = m_dragCentreFrame;
        
        if (frameOff < 0) {
            newCentreFrame -= frameOff;
        } else if (newCentreFrame >= frameOff) {
            newCentreFrame -= frameOff;
        } else {
            newCentreFrame = 0;
        }

#ifdef DEBUG_PANE       
        SVDEBUG << "Pane::dragTopLayer: newCentreFrame = " << newCentreFrame <<
            ", models end frame = " << getModelsEndFrame() << endl;
#endif

        if (newCentreFrame >= getModelsEndFrame()) {
            newCentreFrame = getModelsEndFrame();
            if (newCentreFrame > 0) --newCentreFrame;
        }
                
        if (getXForFrame(m_centreFrame) != getXForFrame(newCentreFrame)) {
            setCentreFrame(newCentreFrame, !m_altPressed);
        }
    }

    if (m_dragMode == VerticalDrag ||
        m_dragMode == FreeDrag) {

        double vmin = 0.f, vmax = 0.f;
        double dmin = 0.f, dmax = 0.f;

        if (getTopLayerDisplayExtents(vmin, vmax, dmin, dmax)) {

//            cerr << "ydiff = " << ydiff << endl;

            int ydiff = e->y() - m_clickPos.y();
            double perpix = (dmax - dmin) / height();
            double valdiff = ydiff * perpix;
//            cerr << "valdiff = " << valdiff << endl;

            if (m_dragMode == UnresolvedDrag && ydiff != 0) {
                m_dragMode = VerticalDrag;
            }

            double newmin = m_dragStartMinValue + valdiff;
            double newmax = m_dragStartMinValue + (dmax - dmin) + valdiff;
            if (newmin < vmin) {
                newmax += vmin - newmin;
                newmin += vmin - newmin;
            }
            if (newmax > vmax) {
                newmin -= newmax - vmax;
                newmax -= newmax - vmax;
            }
//            cerr << "(" << dmin << ", " << dmax << ") -> ("
//                      << newmin << ", " << newmax << ") (drag start " << m_dragStartMinValue << ")" << endl;

            setTopLayerDisplayExtents(newmin, newmax);
            updateVerticalPanner();
        }
    }
}

Pane::DragMode
Pane::updateDragMode(DragMode dragMode,
                     QPoint origin,
                     QPoint point,
                     bool canMoveHorizontal,
                     bool canMoveVertical,
                     bool resistHorizontal,
                     bool resistVertical)
{
    int xdiff = point.x() - origin.x();
    int ydiff = point.y() - origin.y();

    int smallThreshold = 10, bigThreshold = 80;

    if (m_manager) {
        smallThreshold = m_manager->scalePixelSize(smallThreshold);
        bigThreshold = m_manager->scalePixelSize(bigThreshold);
    }

//    SVDEBUG << "Pane::updateDragMode: xdiff = " << xdiff << ", ydiff = "
//              << ydiff << ", canMoveVertical = " << canMoveVertical << ", drag mode = " << m_dragMode << endl;

    if (dragMode == UnresolvedDrag) {

        if (abs(ydiff) > smallThreshold &&
            abs(ydiff) > abs(xdiff) * 2 &&
            canMoveVertical) {
//            SVDEBUG << "Pane::updateDragMode: passed vertical threshold" << endl;
            dragMode = VerticalDrag;
        } else if (abs(xdiff) > smallThreshold &&
                   abs(xdiff) > abs(ydiff) * 2 &&
                   canMoveHorizontal) {
//            SVDEBUG << "Pane::updateDragMode: passed horizontal threshold" << endl;
            dragMode = HorizontalDrag;
        } else if (abs(xdiff) > smallThreshold &&
                   abs(ydiff) > smallThreshold &&
                   canMoveVertical &&
                   canMoveHorizontal) {
//            SVDEBUG << "Pane::updateDragMode: passed both thresholds" << endl;
            dragMode = FreeDrag;
        }
    }

    if (dragMode == VerticalDrag && canMoveHorizontal) {
        if (abs(xdiff) > bigThreshold) dragMode = FreeDrag;
    }

    if (dragMode == HorizontalDrag && canMoveVertical) {
        if (abs(ydiff) > bigThreshold) dragMode = FreeDrag;
    }

    if (dragMode == UnresolvedDrag) {
        if (!resistHorizontal && xdiff != 0) {
            dragMode = HorizontalDrag;
        }
        if (!resistVertical && ydiff != 0) {
            if (dragMode == HorizontalDrag) dragMode = FreeDrag;
            else dragMode = VerticalDrag;
        }
    }
    
    return dragMode;
}

void
Pane::dragExtendSelection(QMouseEvent *e)
{
    sv_frame_t mouseFrame = getFrameForX(e->x());
    int resolution = 1;
    sv_frame_t snapFrameLeft = mouseFrame;
    sv_frame_t snapFrameRight = mouseFrame;
    
    Layer *layer = getInteractionLayer();
    if (layer && !m_shiftPressed &&
        !qobject_cast<TimeRulerLayer *>(layer)) { // don't snap to secs
        layer->snapToFeatureFrame(this, snapFrameLeft,
                                  resolution, Layer::SnapLeft);
        layer->snapToFeatureFrame(this, snapFrameRight,
                                  resolution, Layer::SnapRight);
    }
        
//        cerr << "snap: frame = " << mouseFrame << ", start frame = " << m_selectionStartFrame << ", left = " << snapFrameLeft << ", right = " << snapFrameRight << endl;

    if (snapFrameLeft < 0) snapFrameLeft = 0;
    if (snapFrameRight < 0) snapFrameRight = 0;
    
    sv_frame_t min, max;
    
    if (m_selectionStartFrame > snapFrameLeft) {
        min = snapFrameLeft;
        max = m_selectionStartFrame;
    } else if (snapFrameRight > m_selectionStartFrame) {
        min = m_selectionStartFrame;
        max = snapFrameRight;
    } else {
        min = snapFrameLeft;
        max = snapFrameRight;
    }

    sv_frame_t end = getModelsEndFrame();
    if (min > end) min = end;
    if (max > end) max = end;

    if (m_manager) {

        Selection sel(alignToReference(min), alignToReference(max));

        bool exc;
        bool same = (m_manager->haveInProgressSelection() &&
                     m_manager->getInProgressSelection(exc) == sel);
        
        m_manager->setInProgressSelection(sel, !m_resizing && !m_ctrlPressed);

        if (!same) {
            edgeScrollMaybe(e->x());
        }
    }

    update();

    if (min != max) {
        m_playbackFrameMoveScheduled = false;
    }
}

void
Pane::edgeScrollMaybe(int x)
{
    sv_frame_t mouseFrame = getFrameForX(x);

    bool doScroll = false;
    if (!m_manager) doScroll = true;
    else if (!m_manager->isPlaying()) doScroll = true;

    if (m_followPlay != PlaybackScrollContinuous) doScroll = true;

    if (doScroll) {
        sv_frame_t offset = mouseFrame - getStartFrame();
        sv_frame_t available = getEndFrame() - getStartFrame();
        sv_frame_t move = 0;
        sv_frame_t rightEdge = available - (available / 20);
        sv_frame_t leftEdge = (available / 10);
        if (offset >= rightEdge) {
            move = offset - rightEdge + 1;
        } else if (offset <= leftEdge) {
            move = offset - leftEdge - 1;
        }
        if (move != 0) {
            setCentreFrame(m_centreFrame + move);
            update();
        }
    }
}

void
Pane::mouseDoubleClickEvent(QMouseEvent *e)
{
    if (e->buttons() & Qt::RightButton) {
        return;
    }

    cerr << "mouseDoubleClickEvent" << endl;

    m_clickPos = e->pos();
    m_clickedInRange = true;
    m_shiftPressed = (e->modifiers() & Qt::ShiftModifier);
    m_ctrlPressed = (e->modifiers() & Qt::ControlModifier);
    m_altPressed = (e->modifiers() & Qt::AltModifier);

    // cancel any pending move that came from a single click
    m_playbackFrameMoveScheduled = false;

    ViewManager::ToolMode mode = ViewManager::NavigateMode;
    if (m_manager) mode = m_manager->getToolModeFor(this);

    bool relocate = (mode == ViewManager::NavigateMode ||
                     (e->buttons() & Qt::MidButton));

    if (mode == ViewManager::SelectMode) {
        m_clickedInRange = false;
        if (m_manager) m_manager->clearInProgressSelection();
        emit doubleClickSelectInvoked(getFrameForX(e->x()));
        return;
    }

    if (mode == ViewManager::EditMode ||
        (mode == ViewManager::NavigateMode &&
         m_manager->getOpportunisticEditingEnabled())) {

        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            if (layer->editOpen(this, e)) relocate = false;
        }

    } else if (mode == ViewManager::MeasureMode) {

        Layer *layer = getTopLayer();
        if (layer) layer->measureDoubleClick(this, e);
        update();
    }

    if (relocate) {

        sv_frame_t f = getFrameForX(e->x());

        setCentreFrame(f);

        m_dragCentreFrame = f;
        m_dragStartMinValue = 0;
        m_dragMode = UnresolvedDrag;

        double vmin, vmax, dmin, dmax;
        if (getTopLayerDisplayExtents(vmin, vmax, dmin, dmax)) {
            m_dragStartMinValue = dmin;
        }
    }
    
    if (mode == ViewManager::NoteEditMode) {
        std::cerr << "double click in note edit mode" << std::endl;
        Layer *layer = getInteractionLayer();
        if (layer && layer->isLayerEditable()) {
            layer->addNote(this, e); 
        }
    }

    m_clickedInRange = false; // in case mouseReleaseEvent is not properly called
}

void
Pane::enterEvent(QEvent *)
{
    m_mouseInWidget = true;
}

void
Pane::leaveEvent(QEvent *)
{
    m_mouseInWidget = false;
    bool previouslyIdentifying = m_identifyFeatures;
    m_identifyFeatures = false;
    if (previouslyIdentifying) update();
    emit contextHelpChanged("");
}

void
Pane::resizeEvent(QResizeEvent *)
{
    updateHeadsUpDisplay();
}

void
Pane::wheelEvent(QWheelEvent *e)
{
//    cerr << "wheelEvent, delta " << e->delta() << ", angleDelta " << e->angleDelta().x() << "," << e->angleDelta().y() << ", pixelDelta " << e->pixelDelta().x() << "," << e->pixelDelta().y() << ", modifiers " << e->modifiers() << endl;

    e->accept(); // we never want wheel events on the pane to be propagated
    
    int dx = e->angleDelta().x();
    int dy = e->angleDelta().y();

    if (dx == 0 && dy == 0) {
        return;
    }

    int d = dy;
    bool horizontal = false;

    if (abs(dx) > abs(dy)) {
        d = dx;
        horizontal = true;
    } else if (e->modifiers() & Qt::ControlModifier) {
        // treat a vertical wheel as horizontal
        horizontal = true;
    }

    if (e->phase() == Qt::ScrollBegin ||
        std::abs(d) >= 120 ||
        (d > 0 && m_pendingWheelAngle < 0) ||
        (d < 0 && m_pendingWheelAngle > 0)) {
        m_pendingWheelAngle = d;
    } else {
        m_pendingWheelAngle += d;
    }

    if (horizontal && e->pixelDelta().x() != 0) {

        // Have fine pixel information: use it

        wheelHorizontalFine(e->pixelDelta().x(), e->modifiers());
    
        m_pendingWheelAngle = 0;

    } else {

        // Coarse wheel information (or vertical zoom, which is
        // necessarily coarse itself)

        // Sometimes on Linux we're seeing very extreme angles on the
        // first wheel event. They could be spurious, or they could be
        // a result of the user frantically wheeling away while the
        // pane was unresponsive for some reason. We don't want to
        // discard them, as that makes the application feel even less
        // responsive, but if we take them literally we risk changing
        // the view so radically that the user won't recognise what
        // has happened. Clamp them instead.
        if (m_pendingWheelAngle > 600) {
            m_pendingWheelAngle = 600;
        }
        if (m_pendingWheelAngle < -600) {
            m_pendingWheelAngle = -600;
        }

        while (abs(m_pendingWheelAngle) >= 120) {

            int sign = (m_pendingWheelAngle < 0 ? -1 : 1);

            if (horizontal) {
                wheelHorizontal(sign, e->modifiers());
            } else {
                wheelVertical(sign, e->modifiers());
            }

            m_pendingWheelAngle -= sign * 120;
        }
    }
}

void
Pane::wheelVertical(int sign, Qt::KeyboardModifiers mods)
{
//    cerr << "wheelVertical: sign = " << sign << endl;

    if (mods & Qt::ShiftModifier) {

        // Pan vertically

        if (m_vpan) {
            m_vpan->scroll(sign > 0);
        }

    } else if (mods & Qt::AltModifier) {

        // Zoom vertically

        if (m_vthumb) {
            m_vthumb->scroll(sign > 0);
        }

    } else {
        using namespace std::rel_ops;

        // Zoom in or out

        ZoomLevel newZoomLevel = m_zoomLevel;
  
        if (sign > 0) {
            newZoomLevel = getZoomConstraintLevel(newZoomLevel.decremented(),
                                                  ZoomConstraint::RoundDown);
        } else {
            newZoomLevel = getZoomConstraintLevel(newZoomLevel.incremented(),
                                                  ZoomConstraint::RoundUp);
        }
    
        if (newZoomLevel != m_zoomLevel) {
            setZoomLevel(newZoomLevel);
        }
    }

    emit paneInteractedWith();
}

void
Pane::wheelHorizontal(int sign, Qt::KeyboardModifiers mods)
{
    // Scroll left or right, rapidly

    wheelHorizontalFine(120 * sign, mods);
}

void
Pane::wheelHorizontalFine(int pixels, Qt::KeyboardModifiers)
{
    // Scroll left or right by a fixed number of pixels

    if (getStartFrame() < 0 && 
        getEndFrame() >= getModelsEndFrame()) {
        return;
    }

    int delta = int(round(m_zoomLevel.pixelsToFrames(pixels)));

    if (m_centreFrame < delta) {
        setCentreFrame(0);
    } else if (m_centreFrame - delta >= getModelsEndFrame()) {
        setCentreFrame(getModelsEndFrame());
    } else {
        setCentreFrame(m_centreFrame - delta);
    }

    emit paneInteractedWith();
}

void
Pane::horizontalThumbwheelMoved(int value)
{
    ZoomLevel level = getZoomLevelByIndex(m_hthumb->getMaximumValue() - value);
    setZoomLevel(level);
}    

void
Pane::verticalThumbwheelMoved(int value)
{
    Layer *layer = nullptr;
    if (getLayerCount() > 0) layer = getLayer(getLayerCount() - 1);
    if (layer) {
        int defaultStep = 0;
        int max = layer->getVerticalZoomSteps(defaultStep);
        if (max == 0) {
            updateHeadsUpDisplay();
            return;
        }
        if (value > max) {
            value = max;
        }
        layer->setVerticalZoomStep(value);
        updateVerticalPanner();
    }
}    

void
Pane::verticalPannerMoved(float , float y0, float , float h)
{
    double vmin, vmax, dmin, dmax;
    if (!getTopLayerDisplayExtents(vmin, vmax, dmin, dmax)) return;
    double y1 = y0 + h;
    double newmax = vmin + ((1.0 - y0) * (vmax - vmin));
    double newmin = vmin + ((1.0 - y1) * (vmax - vmin));
//    cerr << "verticalPannerMoved: (" << x0 << "," << y0 << "," << w
//              << "," << h << ") -> (" << newmin << "," << newmax << ")" << endl;
    setTopLayerDisplayExtents(newmin, newmax);
}

void
Pane::editVerticalPannerExtents()
{
    if (!m_vpan || !m_manager || !m_manager->getZoomWheelsEnabled()) return;

    double vmin, vmax, dmin, dmax;
    QString unit;
    if (!getTopLayerDisplayExtents(vmin, vmax, dmin, dmax, &unit)
        || vmax == vmin) {
        return;
    }

    RangeInputDialog dialog(tr("Enter new range"),
                            tr("New vertical display range, from %1 to %2 %4:")
                            .arg(vmin).arg(vmax).arg(unit),
                            unit, float(vmin), float(vmax), this);
    dialog.setRange(float(dmin), float(dmax));

    if (dialog.exec() == QDialog::Accepted) {
        float newmin, newmax;
        dialog.getRange(newmin, newmax);
        setTopLayerDisplayExtents(newmin, newmax);
        updateVerticalPanner();
    }
}

void
Pane::layerParametersChanged()
{
    View::layerParametersChanged();
    updateHeadsUpDisplay();
}

void
Pane::dragEnterEvent(QDragEnterEvent *e)
{
    QStringList formats(e->mimeData()->formats());
    cerr << "dragEnterEvent: format: "
              << formats.join(",")
              << ", possibleActions: " << e->possibleActions()
              << ", proposedAction: " << e->proposedAction() << endl;
    
    if (e->mimeData()->hasFormat("text/uri-list") ||
        e->mimeData()->hasFormat("text/plain")) {

        if (e->proposedAction() & Qt::CopyAction) {
            e->acceptProposedAction();
        } else {
            e->setDropAction(Qt::CopyAction);
            e->accept();
        }
    }
}

void
Pane::dropEvent(QDropEvent *e)
{
    cerr << "dropEvent: text: \"" << e->mimeData()->text()
              << "\"" << endl;

    if (e->mimeData()->hasFormat("text/uri-list") || 
        e->mimeData()->hasFormat("text/plain")) {

        if (e->proposedAction() & Qt::CopyAction) {
            e->acceptProposedAction();
        } else {
            e->setDropAction(Qt::CopyAction);
            e->accept();
        }

        if (e->mimeData()->hasFormat("text/uri-list")) {

            SVDEBUG << "accepting... data is \"" << e->mimeData()->data("text/uri-list").data() << "\"" << endl;
            emit dropAccepted(QString::fromLocal8Bit
                              (e->mimeData()->data("text/uri-list").data())
                              .split(QRegExp("[\\r\\n]+"), 
                                     QString::SkipEmptyParts));
        } else {
            emit dropAccepted(QString::fromLocal8Bit
                              (e->mimeData()->data("text/plain").data()));
        }
    }
}

bool
Pane::editSelectionStart(QMouseEvent *e)
{
    if (!m_identifyFeatures ||
        !m_manager ||
        m_manager->getToolModeFor(this) != ViewManager::EditMode) {
        return false;
    }

    bool closeToLeft, closeToRight;
    Selection s(getSelectionAt(e->x(), closeToLeft, closeToRight));
    if (s.isEmpty()) return false;
    m_editingSelection = s;
    m_editingSelectionEdge = (closeToLeft ? -1 : closeToRight ? 1 : 0);
    m_mousePos = e->pos();
    return true;
}

bool
Pane::editSelectionDrag(QMouseEvent *e)
{
    if (m_editingSelection.isEmpty()) return false;
    m_mousePos = e->pos();
    update();
    return true;
}

bool
Pane::editSelectionEnd(QMouseEvent *)
{
    if (m_editingSelection.isEmpty()) return false;

    int offset = m_mousePos.x() - m_clickPos.x();
    Layer *layer = getInteractionLayer();

    if (offset == 0 || !layer) {
        m_editingSelection = Selection();
        return true;
    }

    int p0 = getXForFrame(m_editingSelection.getStartFrame()) + offset;
    int p1 = getXForFrame(m_editingSelection.getEndFrame()) + offset;

    sv_frame_t f0 = getFrameForX(p0);
    sv_frame_t f1 = getFrameForX(p1);

    Selection newSelection(f0, f1);
    
    if (m_editingSelectionEdge == 0) {
    
        CommandHistory::getInstance()->startCompoundOperation
            (tr("Drag Selection"), true);

        layer->moveSelection(m_editingSelection, f0);
    
    } else {
    
        CommandHistory::getInstance()->startCompoundOperation
            (tr("Resize Selection"), true);

        if (m_editingSelectionEdge < 0) {
            f1 = m_editingSelection.getEndFrame();
        } else {
            f0 = m_editingSelection.getStartFrame();
        }

        newSelection = Selection(f0, f1);
        layer->resizeSelection(m_editingSelection, newSelection);
    }
    
    m_manager->removeSelection(m_editingSelection);
    m_manager->addSelection(newSelection);

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

    m_editingSelection = Selection();
    return true;
}

void
Pane::toolModeChanged()
{
    ViewManager::ToolMode mode = m_manager->getToolModeFor(this);
//    SVDEBUG << "Pane::toolModeChanged(" << mode << ")" << endl;

    if (mode == ViewManager::MeasureMode && !m_measureCursor1) {
        m_measureCursor1 = new QCursor(QBitmap(":/icons/measure1cursor.xbm"),
                                       QBitmap(":/icons/measure1mask.xbm"),
                                       15, 14);
        m_measureCursor2 = new QCursor(QBitmap(":/icons/measure2cursor.xbm"),
                                       QBitmap(":/icons/measure2mask.xbm"),
                                       16, 17);
    }

    switch (mode) {

    case ViewManager::NavigateMode:
        setCursor(Qt::PointingHandCursor);
        break;
    
    case ViewManager::SelectMode:
        setCursor(Qt::ArrowCursor);
        break;
    
    case ViewManager::EditMode:
        setCursor(Qt::UpArrowCursor);
        break;
    
    case ViewManager::DrawMode:
        setCursor(Qt::CrossCursor);
        break;
    
    case ViewManager::EraseMode:
        setCursor(Qt::CrossCursor);
        break;

    case ViewManager::MeasureMode:
        if (m_measureCursor1) setCursor(*m_measureCursor1);
        break;

        // GF: NoteEditMode uses the same default cursor as EditMode, but it will change in a context sensitive manner.
    case ViewManager::NoteEditMode:
        setCursor(Qt::UpArrowCursor);
        break;

/*  
    case ViewManager::TextMode:
    setCursor(Qt::IBeamCursor);
    break;
*/
    }
}

void
Pane::zoomWheelsEnabledChanged()
{
    updateHeadsUpDisplay();
    update();
}

void
Pane::viewZoomLevelChanged(View *v, ZoomLevel z, bool locked)
{
//    cerr << "Pane[" << this << "]::zoomLevelChanged (global now "
//              << (m_manager ? m_manager->getGlobalZoom() : 0) << ")" << endl;

    View::viewZoomLevelChanged(v, z, locked);

    if (m_hthumb && !m_hthumb->isVisible()) return;

    if (v != this) {
        if (!locked || !m_followZoom) return;
    }

    if (m_manager && m_manager->getZoomWheelsEnabled()) {
        updateHeadsUpDisplay();
    }
}

void
Pane::propertyContainerSelected(View *v, PropertyContainer *pc)
{
    Layer *layer = nullptr;

    if (getLayerCount() > 0) {
        layer = getLayer(getLayerCount() - 1);
        disconnect(layer, SIGNAL(verticalZoomChanged()),
                   this, SLOT(verticalZoomChanged()));
    }

    View::propertyContainerSelected(v, pc);
    updateHeadsUpDisplay();

    if (m_vthumb) {
        RangeMapper *rm = nullptr;
        if (layer) rm = layer->getNewVerticalZoomRangeMapper();
        if (rm) m_vthumb->setRangeMapper(rm);
    }

    if (getLayerCount() > 0) {
        layer = getLayer(getLayerCount() - 1);
        connect(layer, SIGNAL(verticalZoomChanged()),
                this, SLOT(verticalZoomChanged()));
    }
}

void
Pane::verticalZoomChanged()
{
    Layer *layer = nullptr;

    if (getLayerCount() > 0) {

        layer = getLayer(getLayerCount() - 1);

        if (m_vthumb && m_vthumb->isVisible()) {
            m_vthumb->setValue(layer->getCurrentVerticalZoomStep());
        }
    }
}

void
Pane::updateContextHelp(const QPoint *pos)
{
    QString help = "";

    if (m_clickedInRange) {
        emit contextHelpChanged("");
        return;
    }

    ViewManager::ToolMode mode = ViewManager::NavigateMode;
    if (m_manager) mode = m_manager->getToolModeFor(this);

    bool editable = false;
    Layer *layer = getInteractionLayer();
    if (layer && layer->isLayerEditable()) {
        editable = true;
    }
        
    if (mode == ViewManager::NavigateMode) {

        help = tr("Click and drag to navigate; use mouse-wheel or trackpad-scroll to zoom; hold Shift and drag to zoom to an area");
        
    } else if (mode == ViewManager::SelectMode) {

        if (!hasTopLayerTimeXAxis()) return;

        bool haveSelection = (m_manager && !m_manager->getSelections().empty());

        if (haveSelection) {
#ifdef Q_OS_MAC
            if (editable) {
                help = tr("Click and drag to select a range; hold Shift to avoid snapping to items; hold Cmd for multi-select; middle-click and drag to navigate");
            } else {
                help = tr("Click and drag to select a range; hold Cmd for multi-select; middle-click and drag to navigate");
            }
#else
            if (editable) {
                help = tr("Click and drag to select a range; hold Shift to avoid snapping to items; hold Ctrl for multi-select; middle-click and drag to navigate");
            } else {
                help = tr("Click and drag to select a range; hold Ctrl for multi-select; middle-click and drag to navigate");
            }                
#endif

            if (pos) {
                bool closeToLeft = false, closeToRight = false;
                Selection selection = getSelectionAt(pos->x(), closeToLeft, closeToRight);
                if ((closeToLeft || closeToRight) && !(closeToLeft && closeToRight)) {
                    
                    help = tr("Click and drag to move the selection boundary");
                }
            }
        } else {
            if (editable) {
                help = tr("Click and drag to select a range; hold Shift to avoid snapping to items; middle-click to navigate");
            } else {
                help = tr("Click and drag to select a range; middle-click and drag to navigate");
            }
        }

    } else if (mode == ViewManager::DrawMode) {
        
        //!!! could call through to a layer function to find out exact meaning
        if (editable) {
            help = tr("Click to add a new item in the active layer");
        }

    } else if (mode == ViewManager::EraseMode) {
        
        //!!! could call through to a layer function to find out exact meaning
        if (editable) {
            help = tr("Click to erase an item from the active layer");
        }
        
    } else if (mode == ViewManager::EditMode) {
        
        //!!! could call through to layer
        if (editable) {
            help = tr("Click and drag an item in the active layer to move it; hold Shift to override initial resistance");
            if (pos) {
                bool closeToLeft = false, closeToRight = false;
                Selection selection = getSelectionAt(pos->x(), closeToLeft, closeToRight);
                if (!selection.isEmpty()) {
                    help = tr("Click and drag to move all items in the selected range");
                }
            }
        }
    }

    emit contextHelpChanged(help);
}

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

    if (w == m_vpan) {
        emit contextHelpChanged(tr("Click and drag to adjust the visible range of the vertical scale"));
    } else if (w == m_vthumb) {
        emit contextHelpChanged(tr("Click and drag to adjust the vertical zoom level"));
    } else if (w == m_hthumb) {
        emit contextHelpChanged(tr("Click and drag to adjust the horizontal zoom level"));
    } else if (w == m_reset) {
        emit contextHelpChanged(tr("Reset horizontal and vertical zoom levels to their defaults"));
    }
}

void
Pane::mouseLeftWidget()
{
    emit contextHelpChanged("");
}

void
Pane::toXml(QTextStream &stream,
            QString indent, QString extraAttributes) const
{
    View::toXml
        (stream, indent,
     QString("type=\"pane\" centreLineVisible=\"%1\" height=\"%2\" %3")
     .arg(m_centreLineVisible).arg(height()).arg(extraAttributes));
}