view layer/BoxLayer.cpp @ 1551:e79731086b0f

Fixes to NoteLayer, particularly to calculation of vertical scale when model unit is not Hz. To avoid inconsistency we now behave as if the unit is always Hz from the point of view of the external API and display, converting at the point where we obtain values from the events themselves. Also various fixes to editing.
author Chris Cannam
date Thu, 21 Nov 2019 14:02:57 +0000
parents e6362cf5ff1d
children e95cefd4aa81
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 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 "BoxLayer.h"

#include "data/model/Model.h"
#include "base/RealTime.h"
#include "base/Profiler.h"
#include "base/LogRange.h"

#include "ColourDatabase.h"
#include "ColourMapper.h"
#include "LinearNumericalScale.h"
#include "LogNumericalScale.h"
#include "PaintAssistant.h"

#include "view/View.h"

#include "data/model/BoxModel.h"

#include "widgets/ItemEditDialog.h"
#include "widgets/TextAbbrev.h"

#include <QPainter>
#include <QPainterPath>
#include <QMouseEvent>
#include <QTextStream>
#include <QMessageBox>

#include <iostream>
#include <cmath>

BoxLayer::BoxLayer() :
    SingleColourLayer(),
    m_editing(false),
    m_dragPointX(0),
    m_dragPointY(0),
    m_dragStartX(0),
    m_dragStartY(0),
    m_originalPoint(0, 0.0, 0, tr("New Box")),
    m_editingPoint(0, 0.0, 0, tr("New Box")),
    m_editingCommand(nullptr),
    m_verticalScale(AutoAlignScale)
{
    
}

int
BoxLayer::getCompletion(LayerGeometryProvider *) const
{
    auto model = ModelById::get(m_model);
    if (model) return model->getCompletion();
    else return 0;
}

void
BoxLayer::setModel(ModelId modelId)
{
    auto oldModel = ModelById::getAs<BoxModel>(m_model);
    auto newModel = ModelById::getAs<BoxModel>(modelId);
    
    if (!modelId.isNone() && !newModel) {
        throw std::logic_error("Not a BoxModel");
    }
    
    if (m_model == modelId) return;
    m_model = modelId;

    if (newModel) {
        connectSignals(m_model);
    }
    
    emit modelReplaced();
}

Layer::PropertyList
BoxLayer::getProperties() const
{
    PropertyList list = SingleColourLayer::getProperties();
    list.push_back("Vertical Scale");
    list.push_back("Scale Units");
    return list;
}

QString
BoxLayer::getPropertyLabel(const PropertyName &name) const
{
    if (name == "Vertical Scale") return tr("Vertical Scale");
    if (name == "Scale Units") return tr("Scale Units");
    return SingleColourLayer::getPropertyLabel(name);
}

Layer::PropertyType
BoxLayer::getPropertyType(const PropertyName &name) const
{
    if (name == "Vertical Scale") return ValueProperty;
    if (name == "Scale Units") return UnitsProperty;
    return SingleColourLayer::getPropertyType(name);
}

QString
BoxLayer::getPropertyGroupName(const PropertyName &name) const
{
    if (name == "Vertical Scale" || name == "Scale Units") {
        return tr("Scale");
    }
    return SingleColourLayer::getPropertyGroupName(name);
}

int
BoxLayer::getPropertyRangeAndValue(const PropertyName &name,
                                    int *min, int *max, int *deflt) const
{
    int val = 0;

    if (name == "Vertical Scale") {
        
        if (min) *min = 0;
        if (max) *max = 2;
        if (deflt) *deflt = int(LinearScale);
        
        val = int(m_verticalScale);

    } else if (name == "Scale Units") {

        if (deflt) *deflt = 0;
        auto model = ModelById::getAs<BoxModel>(m_model);
        if (model) {
            val = UnitDatabase::getInstance()->getUnitId
                (model->getScaleUnits());
        }

    } else {
        val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
    }

    return val;
}

QString
BoxLayer::getPropertyValueLabel(const PropertyName &name,
                                   int value) const
{
    if (name == "Vertical Scale") {
        switch (value) {
        default:
        case 0: return tr("Auto-Align");
        case 1: return tr("Linear");
        case 2: return tr("Log");
        }
    }
    return SingleColourLayer::getPropertyValueLabel(name, value);
}

void
BoxLayer::setProperty(const PropertyName &name, int value)
{
    if (name == "Vertical Scale") {
        setVerticalScale(VerticalScale(value));
    } else if (name == "Scale Units") {
        auto model = ModelById::getAs<BoxModel>(m_model);
        if (model) {
            model->setScaleUnits
                (UnitDatabase::getInstance()->getUnitById(value));
            emit modelChanged(m_model);
        }
    } else {
        return SingleColourLayer::setProperty(name, value);
    }
}

void
BoxLayer::setVerticalScale(VerticalScale scale)
{
    if (m_verticalScale == scale) return;
    m_verticalScale = scale;
    emit layerParametersChanged();
}

bool
BoxLayer::isLayerScrollable(const LayerGeometryProvider *v) const
{
    QPoint discard;
    return !v->shouldIlluminateLocalFeatures(this, discard);
}

bool
BoxLayer::getValueExtents(double &min, double &max,
                          bool &logarithmic, QString &unit) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return false;
    min = model->getValueMinimum();
    max = model->getValueMaximum();
    unit = getScaleUnits();

    if (m_verticalScale == LogScale) logarithmic = true;

    return true;
}

bool
BoxLayer::getDisplayExtents(double &min, double &max) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || m_verticalScale == AutoAlignScale) return false;

    min = model->getValueMinimum();
    max = model->getValueMaximum();

    return true;
}

bool
BoxLayer::adoptExtents(double min, double max, QString unit)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return false;

    SVDEBUG << "BoxLayer[" << this << "]::adoptExtents: min " << min
            << ", max " << max << ", unit " << unit << endl;
    
    if (model->getScaleUnits() == "") {
        model->setScaleUnits(unit);
        return true;
    } else {
        return false;
    }
}

bool
BoxLayer::getLocalPoint(LayerGeometryProvider *v, int x, int y,
                        Event &point) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !model->isReady()) return false;

    sv_frame_t frame = v->getFrameForX(x);

    EventVector onPoints = model->getEventsCovering(frame);
    if (onPoints.empty()) return false;

    Event bestContaining;
    for (const auto &p: onPoints) {
        auto r = getRange(p);
        if (y > getYForValue(v, r.first) || y < getYForValue(v, r.second)) {
            SVCERR << "inPoints: rejecting " << p.toXmlString() << endl;
            continue;
        }
        SVCERR << "inPoints: looking at " << p.toXmlString() << endl;
        if (bestContaining == Event()) {
            bestContaining = p;
            continue;
        }
        auto br = getRange(bestContaining);
        if (r.first < br.first && r.second > br.second) {
            continue;
        }
        if (r.first > br.first && r.second < br.second) {
            bestContaining = p;
            continue;
        }
        if (p.getFrame() > bestContaining.getFrame() &&
            p.getFrame() + p.getDuration() <
            bestContaining.getFrame() + bestContaining.getDuration()) {
            bestContaining = p;
            continue;
        }
    }

    if (bestContaining != Event()) {
        point = bestContaining;
    } else {
        int nearestDistance = -1;
        for (const auto &p: onPoints) {
            const auto r = getRange(p);
            int distance = std::min
                (getYForValue(v, r.first) - y,
                 getYForValue(v, r.second) - y);
            if (distance < 0) distance = -distance;
            if (nearestDistance == -1 || distance < nearestDistance) {
                nearestDistance = distance;
                point = p;
            }
        }
    }

    return true;
}

QString
BoxLayer::getLabelPreceding(sv_frame_t frame) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return "";
    EventVector points = model->getEventsStartingWithin
        (model->getStartFrame(), frame - model->getStartFrame());
    if (!points.empty()) {
        for (auto i = points.rbegin(); i != points.rend(); ++i) {
            if (i->getLabel() != QString()) {
                return i->getLabel();
            }
        }
    }
    return QString();
}

QString
BoxLayer::getFeatureDescription(LayerGeometryProvider *v,
                                QPoint &pos) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !model->getSampleRate()) return "";
    
    Event box;
    
    if (!getLocalPoint(v, pos.x(), pos.y(), box)) {
        if (!model->isReady()) {
            return tr("In progress");
        } else {
            return tr("No local points");
        }
    }

    RealTime rt = RealTime::frame2RealTime(box.getFrame(),
                                           model->getSampleRate());
    RealTime rd = RealTime::frame2RealTime(box.getDuration(),
                                           model->getSampleRate());
    
    QString rangeText;
    auto r = getRange(box);
    
    rangeText = tr("%1 %2 - %3 %4")
        .arg(r.first).arg(getScaleUnits())
        .arg(r.second).arg(getScaleUnits());

    QString text;

    if (box.getLabel() == "") {
        text = QString(tr("Time:\t%1\nDuration:\t%2\nValue:\t%3\nNo label"))
            .arg(rt.toText(true).c_str())
            .arg(rd.toText(true).c_str())
            .arg(rangeText);
    } else {
        text = QString(tr("Time:\t%1\nDuration:\t%2\nValue:\t%3\nLabel:\t%4"))
            .arg(rt.toText(true).c_str())
            .arg(rd.toText(true).c_str())
            .arg(rangeText)
            .arg(box.getLabel());
    }

    pos = QPoint(v->getXForFrame(box.getFrame()),
                 getYForValue(v, box.getValue()));
    return text;
}

bool
BoxLayer::snapToFeatureFrame(LayerGeometryProvider *v,
                             sv_frame_t &frame,
                             int &resolution,
                             SnapType snap,
                             int ycoord) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) {
        return Layer::snapToFeatureFrame(v, frame, resolution, snap, ycoord);
    }

    // SnapLeft / SnapRight: return frame of nearest feature in that
    // direction no matter how far away
    //
    // SnapNeighbouring: return frame of feature that would be used in
    // an editing operation, i.e. closest feature in either direction
    // but only if it is "close enough"

    resolution = model->getResolution();

    Event containing;

    if (getLocalPoint(v, v->getXForFrame(frame), ycoord, containing)) {

        switch (snap) {

        case SnapLeft:
        case SnapNeighbouring:
            frame = containing.getFrame();
            return true;

        case SnapRight:
            frame = containing.getFrame() + containing.getDuration();
            return true;
        }
    }
    
    if (snap == SnapNeighbouring) {
        return false;
    }    

    // We aren't actually contained (in time) by any single event, so
    // seek the next one in the relevant direction

    Event e;
    
    if (snap == SnapLeft) {
        if (model->getNearestEventMatching
            (frame, [](Event) { return true; }, EventSeries::Backward, e)) {

            if (e.getFrame() + e.getDuration() < frame) {
                frame = e.getFrame() + e.getDuration();
            } else {
                frame = e.getFrame();
            }
            return true;
        }
    }
    
    if (snap == SnapRight) {
        if (model->getNearestEventMatching
            (frame, [](Event) { return true; }, EventSeries::Forward, e)) {

            frame = e.getFrame();
            return true;
        }
    }

    return false;
}

QString
BoxLayer::getScaleUnits() const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (model) return model->getScaleUnits();
    else return "";
}

void
BoxLayer::getScaleExtents(LayerGeometryProvider *v,
                                       double &min, double &max,
                                       bool &log) const
{
    min = 0.0;
    max = 0.0;
    log = false;

    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return;

    QString queryUnits;
    queryUnits = getScaleUnits();

    if (m_verticalScale == AutoAlignScale) {

        if (!v->getVisibleExtentsForUnit(queryUnits, min, max, log)) {

            min = model->getValueMinimum();
            max = model->getValueMaximum();

//            cerr << "BoxLayer[" << this << "]::getScaleExtents: min = " << min << ", max = " << max << ", log = " << log << endl;

        } else if (log) {

            LogRange::mapRange(min, max);

//            cerr << "BoxLayer[" << this << "]::getScaleExtents: min = " << min << ", max = " << max << ", log = " << log << endl;

        }

    } else {

        min = model->getValueMinimum();
        max = model->getValueMaximum();

        if (m_verticalScale == LogScale) {
            LogRange::mapRange(min, max);
            log = true;
        }
    }

    if (max == min) max = min + 1.0;
}

int
BoxLayer::getYForValue(LayerGeometryProvider *v, double val) const
{
    double min = 0.0, max = 0.0;
    bool logarithmic = false;
    int h = v->getPaintHeight();

    getScaleExtents(v, min, max, logarithmic);

//    cerr << "BoxLayer[" << this << "]::getYForValue(" << val << "): min = " << min << ", max = " << max << ", log = " << logarithmic << endl;
//    cerr << "h = " << h << ", margin = " << margin << endl;

    if (logarithmic) {
        val = LogRange::map(val);
    }

    return int(h - ((val - min) * h) / (max - min));
}

double
BoxLayer::getValueForY(LayerGeometryProvider *v, int y) const
{
    double min = 0.0, max = 0.0;
    bool logarithmic = false;
    int h = v->getPaintHeight();

    getScaleExtents(v, min, max, logarithmic);

    double val = min + (double(h - y) * double(max - min)) / h;

    if (logarithmic) {
        val = pow(10.0, val);
    }

    return val;
}

void
BoxLayer::paint(LayerGeometryProvider *v, QPainter &paint,
                             QRect rect) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !model->isOK()) return;

    sv_samplerate_t sampleRate = model->getSampleRate();
    if (!sampleRate) return;

//    Profiler profiler("BoxLayer::paint", true);

    int x0 = rect.left() - 40, x1 = rect.right();

    sv_frame_t wholeFrame0 = v->getFrameForX(0);
    sv_frame_t wholeFrame1 = v->getFrameForX(v->getPaintWidth());

    EventVector points(model->getEventsSpanning(wholeFrame0,
                                                wholeFrame1 - wholeFrame0));
    if (points.empty()) return;

    paint.setPen(getBaseQColor());

//    SVDEBUG << "BoxLayer::paint: resolution is "
//              << model->getResolution() << " frames" << endl;

    double min = model->getValueMinimum();
    double max = model->getValueMaximum();
    if (max == min) max = min + 1.0;

    QPoint localPos;
    Event illuminatePoint(0);
    bool shouldIlluminate = false;

    if (v->shouldIlluminateLocalFeatures(this, localPos)) {
        shouldIlluminate = getLocalPoint(v, localPos.x(), localPos.y(),
                                         illuminatePoint);
    }

    paint.save();
    paint.setRenderHint(QPainter::Antialiasing, false);

    QFontMetrics fm = paint.fontMetrics();
        
    for (EventVector::const_iterator i = points.begin();
         i != points.end(); ++i) {

        const Event &p(*i);
        const auto r = getRange(p);

        int x = v->getXForFrame(p.getFrame());
        int w = v->getXForFrame(p.getFrame() + p.getDuration()) - x;
        int y = getYForValue(v, r.first);
        int h = getYForValue(v, r.second) - y;
        int ex = x + w;
        int gap = v->scalePixelSize(2);

        EventVector::const_iterator j = i;
        ++j;

        if (j != points.end()) {
            const Event &q(*j);
            int nx = v->getXForFrame(q.getFrame());
            if (nx < ex) ex = nx;
        }

        if (w < 1) w = 1;

        paint.setPen(getBaseQColor());
        paint.setBrush(Qt::NoBrush);

        if ((shouldIlluminate && illuminatePoint == p) ||
            (m_editing && m_editingPoint == p)) {

            paint.setPen(QPen(getBaseQColor(), v->scalePixelSize(2)));
                
            // 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"

            if (abs(h) > 2 * fm.height()) {
            
                QString y0label = QString("%1 %2")
                    .arg(r.first)
                    .arg(getScaleUnits());

                QString y1label = QString("%1 %2")
                    .arg(r.second)
                    .arg(getScaleUnits());

                PaintAssistant::drawVisibleText
                    (v, paint, 
                     x - fm.width(y0label) - gap,
                     y - fm.descent(), 
                     y0label, PaintAssistant::OutlinedText);

                PaintAssistant::drawVisibleText
                    (v, paint, 
                     x - fm.width(y1label) - gap,
                     y + h + fm.ascent(), 
                     y1label, PaintAssistant::OutlinedText);

            } else {
            
                QString ylabel = QString("%1 %2 - %3 %4")
                    .arg(r.first)
                    .arg(getScaleUnits())
                    .arg(r.second)
                    .arg(getScaleUnits());

                PaintAssistant::drawVisibleText
                    (v, paint, 
                     x - fm.width(ylabel) - gap,
                     y - fm.descent(), 
                     ylabel, PaintAssistant::OutlinedText);
            }
            
            QString t0label = RealTime::frame2RealTime
                (p.getFrame(), model->getSampleRate()).toText(true).c_str();

            QString t1label = RealTime::frame2RealTime
                (p.getFrame() + p.getDuration(), model->getSampleRate())
                .toText(true).c_str();

            PaintAssistant::drawVisibleText
                (v, paint, x, y + fm.ascent() + gap,
                 t0label, PaintAssistant::OutlinedText);

            if (w > fm.width(t0label) + fm.width(t1label) + gap * 3) {

                PaintAssistant::drawVisibleText
                    (v, paint,
                     x + w - fm.width(t1label),
                     y + fm.ascent() + gap,
                     t1label, PaintAssistant::OutlinedText);

            } else {

                PaintAssistant::drawVisibleText
                    (v, paint,
                     x + w - fm.width(t1label),
                     y + fm.ascent() + fm.height() + gap,
                     t1label, PaintAssistant::OutlinedText);
            }                
        }

        paint.drawRect(x, y, w, h);
    }

    for (EventVector::const_iterator i = points.begin();
         i != points.end(); ++i) {

        const Event &p(*i);

        QString label = p.getLabel();
        if (label == "") continue;

        if (shouldIlluminate && illuminatePoint == p) {
            continue; // already handled this in illumination special case
        }
        
        int x = v->getXForFrame(p.getFrame());
        int w = v->getXForFrame(p.getFrame() + p.getDuration()) - x;
        int y = getYForValue(v, p.getValue());

        int labelWidth = fm.width(label);

        int gap = v->scalePixelSize(2);

        if (x + w < x0 || x - labelWidth - gap > x1) {
            continue;
        }
        
        int labelX, labelY;

        labelX = x - labelWidth - gap;
        labelY = y - fm.descent();

        PaintAssistant::drawVisibleText(v, paint, labelX, labelY, label,
                                        PaintAssistant::OutlinedText);
    }

    paint.restore();
}

int
BoxLayer::getVerticalScaleWidth(LayerGeometryProvider *v,
                                             bool, QPainter &paint) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || m_verticalScale == AutoAlignScale) {
        return 0;
    } else {
        if (m_verticalScale == LogScale) {
            return LogNumericalScale().getWidth(v, paint);
        } else {
            return LinearNumericalScale().getWidth(v, paint);
        }
    }
}

void
BoxLayer::paintVerticalScale(LayerGeometryProvider *v,
                                          bool, QPainter &paint, QRect) const
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || model->isEmpty()) return;

    QString unit;
    double min, max;
    bool logarithmic;

    int w = getVerticalScaleWidth(v, false, paint);

    getScaleExtents(v, min, max, logarithmic);

    if (logarithmic) {
        LogNumericalScale().paintVertical(v, this, paint, 0, min, max);
    } else {
        LinearNumericalScale().paintVertical(v, this, paint, 0, min, max);
    }
        
    if (getScaleUnits() != "") {
        int mw = w - 5;
        paint.drawText(5,
                       5 + paint.fontMetrics().ascent(),
                       TextAbbrev::abbreviate(getScaleUnits(),
                                              paint.fontMetrics(),
                                              mw));
    }
}

void
BoxLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return;

    sv_frame_t frame = v->getFrameForX(e->x());
    if (frame < 0) frame = 0;
    frame = frame / model->getResolution() * model->getResolution();

    double value = getValueForY(v, e->y());

    m_editingPoint = Event(frame, float(value), 0, "");
    m_originalPoint = m_editingPoint;

    if (m_editingCommand) finish(m_editingCommand);
    m_editingCommand = new ChangeEventsCommand(m_model.untyped,
                                               tr("Draw Box"));
    m_editingCommand->add(m_editingPoint);

    m_editing = true;
}

void
BoxLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !m_editing) return;

    sv_frame_t dragFrame = v->getFrameForX(e->x());
    if (dragFrame < 0) dragFrame = 0;
    dragFrame = dragFrame / model->getResolution() * model->getResolution();

    sv_frame_t eventFrame = m_originalPoint.getFrame();
    sv_frame_t eventDuration = dragFrame - eventFrame;
    if (eventDuration < 0) {
        eventFrame = eventFrame + eventDuration;
        eventDuration = -eventDuration;
    } else if (eventDuration == 0) {
        eventDuration = model->getResolution();
    }

    double dragValue = getValueForY(v, e->y());

    double eventValue = m_originalPoint.getValue();
    double eventFreqDiff = dragValue - eventValue;
    if (eventFreqDiff < 0) {
        eventValue = eventValue + eventFreqDiff;
        eventFreqDiff = -eventFreqDiff;
    }

    m_editingCommand->remove(m_editingPoint);
    m_editingPoint = m_editingPoint
        .withFrame(eventFrame)
        .withDuration(eventDuration)
        .withValue(float(eventValue))
        .withLevel(float(eventFreqDiff));
    m_editingCommand->add(m_editingPoint);
}

void
BoxLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !m_editing) return;
    finish(m_editingCommand);
    m_editingCommand = nullptr;
    m_editing = false;
}

void
BoxLayer::eraseStart(LayerGeometryProvider *v, QMouseEvent *e)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return;

    if (!getLocalPoint(v, e->x(), e->y(), m_editingPoint)) return;

    if (m_editingCommand) {
        finish(m_editingCommand);
        m_editingCommand = nullptr;
    }

    m_editing = true;
}

void
BoxLayer::eraseDrag(LayerGeometryProvider *, QMouseEvent *)
{
}

void
BoxLayer::eraseEnd(LayerGeometryProvider *v, QMouseEvent *e)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !m_editing) return;

    m_editing = false;

    Event p(0);
    if (!getLocalPoint(v, e->x(), e->y(), p)) return;
    if (p.getFrame() != m_editingPoint.getFrame() ||
        p.getValue() != m_editingPoint.getValue()) return;

    m_editingCommand = new ChangeEventsCommand
        (m_model.untyped, tr("Erase Box"));

    m_editingCommand->remove(m_editingPoint);

    finish(m_editingCommand);
    m_editingCommand = nullptr;
    m_editing = false;
}

void
BoxLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return;

    if (!getLocalPoint(v, e->x(), e->y(), m_editingPoint)) {
        return;
    }

    m_dragPointX = v->getXForFrame(m_editingPoint.getFrame());
    m_dragPointY = getYForValue(v, m_editingPoint.getValue());

    m_originalPoint = m_editingPoint;

    if (m_editingCommand) {
        finish(m_editingCommand);
        m_editingCommand = nullptr;
    }

    m_editing = true;
    m_dragStartX = e->x();
    m_dragStartY = e->y();
}

void
BoxLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !m_editing) return;

    int xdist = e->x() - m_dragStartX;
    int ydist = e->y() - m_dragStartY;
    int newx = m_dragPointX + xdist;
    int newy = m_dragPointY + ydist;

    sv_frame_t frame = v->getFrameForX(newx);
    if (frame < 0) frame = 0;
    frame = frame / model->getResolution() * model->getResolution();

    double value = getValueForY(v, newy);

    if (!m_editingCommand) {
        m_editingCommand = new ChangeEventsCommand
            (m_model.untyped,
             tr("Drag Box"));
    }

    m_editingCommand->remove(m_editingPoint);
    m_editingPoint = m_editingPoint
        .withFrame(frame)
        .withValue(float(value));
    m_editingCommand->add(m_editingPoint);
}

void
BoxLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !m_editing) return;

    if (m_editingCommand) {

        QString newName = m_editingCommand->getName();

        if (m_editingPoint.getFrame() != m_originalPoint.getFrame()) {
            if (m_editingPoint.getValue() != m_originalPoint.getValue()) {
                newName = tr("Edit Box");
            } else {
                newName = tr("Relocate Box");
            }
        } else {
            newName = tr("Change Point Value");
        }

        m_editingCommand->setName(newName);
        finish(m_editingCommand);
    }

    m_editingCommand = nullptr;
    m_editing = false;
}

bool
BoxLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return false;

    Event region(0);
    if (!getLocalPoint(v, e->x(), e->y(), region)) return false;

    ItemEditDialog::LabelOptions labelOptions;
    labelOptions.valueLabel = tr("Minimum Value");
    labelOptions.levelLabel = tr("Value Extent");
    labelOptions.valueUnits = getScaleUnits();
    labelOptions.levelUnits = getScaleUnits();
    
    ItemEditDialog *dialog = new ItemEditDialog
        (model->getSampleRate(),
         ItemEditDialog::ShowTime |
         ItemEditDialog::ShowDuration |
         ItemEditDialog::ShowValue |
         ItemEditDialog::ShowLevel |
         ItemEditDialog::ShowText,
         labelOptions);

    dialog->setFrameTime(region.getFrame());
    dialog->setValue(region.getValue());
    dialog->setLevel(region.getLevel());
    dialog->setFrameDuration(region.getDuration());
    dialog->setText(region.getLabel());

    if (dialog->exec() == QDialog::Accepted) {

        Event newBox = region
            .withFrame(dialog->getFrameTime())
            .withValue(dialog->getValue())
            .withLevel(dialog->getLevel())
            .withDuration(dialog->getFrameDuration())
            .withLabel(dialog->getText());
        
        ChangeEventsCommand *command = new ChangeEventsCommand
            (m_model.untyped, tr("Edit Box"));
        command->remove(region);
        command->add(newBox);
        finish(command);
    }

    delete dialog;
    return true;
}

void
BoxLayer::moveSelection(Selection s, sv_frame_t newStartFrame)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return;

    ChangeEventsCommand *command =
        new ChangeEventsCommand(m_model.untyped, tr("Drag Selection"));

    EventVector points =
        model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());

    for (EventVector::iterator i = points.begin();
         i != points.end(); ++i) {

        Event newPoint = (*i)
            .withFrame(i->getFrame() + newStartFrame - s.getStartFrame());
        command->remove(*i);
        command->add(newPoint);
    }

    finish(command);
}

void
BoxLayer::resizeSelection(Selection s, Selection newSize)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model || !s.getDuration()) return;

    ChangeEventsCommand *command =
        new ChangeEventsCommand(m_model.untyped, tr("Resize Selection"));

    EventVector points =
        model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());

    double ratio = double(newSize.getDuration()) / double(s.getDuration());
    double oldStart = double(s.getStartFrame());
    double newStart = double(newSize.getStartFrame());
    
    for (Event p: points) {

        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
        double newDuration = double(p.getDuration()) * ratio;

        Event newPoint = p
            .withFrame(lrint(newFrame))
            .withDuration(lrint(newDuration));
        command->remove(p);
        command->add(newPoint);
    }

    finish(command);
}

void
BoxLayer::deleteSelection(Selection s)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return;

    ChangeEventsCommand *command =
        new ChangeEventsCommand(m_model.untyped, tr("Delete Selected Points"));

    EventVector points =
        model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());

    for (EventVector::iterator i = points.begin();
         i != points.end(); ++i) {

        if (s.contains(i->getFrame())) {
            command->remove(*i);
        }
    }

    finish(command);
}    

void
BoxLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return;

    EventVector points =
        model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());

    for (Event p: points) {
        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
    }
}

bool
BoxLayer::paste(LayerGeometryProvider *v, const Clipboard &from,
                sv_frame_t /* frameOffset */, bool /* interactive */)
{
    auto model = ModelById::getAs<BoxModel>(m_model);
    if (!model) return false;

    const EventVector &points = from.getPoints();

    bool realign = false;

    if (clipboardHasDifferentAlignment(v, from)) {

        QMessageBox::StandardButton button =
            QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
                                  tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                  QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                  QMessageBox::Yes);

        if (button == QMessageBox::Cancel) {
            return false;
        }

        if (button == QMessageBox::Yes) {
            realign = true;
        }
    }

    ChangeEventsCommand *command =
        new ChangeEventsCommand(m_model.untyped, tr("Paste"));

    for (EventVector::const_iterator i = points.begin();
         i != points.end(); ++i) {
        
        sv_frame_t frame = 0;

        if (!realign) {
            
            frame = i->getFrame();

        } else {

            if (i->hasReferenceFrame()) {
                frame = i->getReferenceFrame();
                frame = alignFromReference(v, frame);
            } else {
                frame = i->getFrame();
            }
        }

        Event p = i->withFrame(frame);
        
        Event newPoint = p;
        if (!p.hasValue()) {
            newPoint = newPoint.withValue((model->getValueMinimum() +
                                           model->getValueMaximum()) / 2);
        }
        if (!p.hasDuration()) {
            sv_frame_t nextFrame = frame;
            EventVector::const_iterator j = i;
            for (; j != points.end(); ++j) {
                if (j != i) break;
            }
            if (j != points.end()) {
                nextFrame = j->getFrame();
            }
            if (nextFrame == frame) {
                newPoint = newPoint.withDuration(model->getResolution());
            } else {
                newPoint = newPoint.withDuration(nextFrame - frame);
            }
        }
        
        command->add(newPoint);
    }

    finish(command);
    return true;
}

void
BoxLayer::toXml(QTextStream &stream,
                             QString indent, QString extraAttributes) const
{
    QString s;

    s += QString("verticalScale=\"%1\" ").arg(m_verticalScale);
    
    SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s);
}

void
BoxLayer::setProperties(const QXmlAttributes &attributes)
{
    SingleColourLayer::setProperties(attributes);

    bool ok;
    VerticalScale scale = (VerticalScale)
        attributes.value("verticalScale").toInt(&ok);
    if (ok) setVerticalScale(scale);
}