view widgets/LevelPanWidget.cpp @ 967:faa8532e4f02

Fix arithmetic precedence misreading goof which led to scrolling the wrong way when dragging past the right edge
author Chris Cannam
date Thu, 21 May 2015 14:41:28 +0100
parents d6acb8e36605
children 125748a569fa
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 "LevelPanWidget.h"

#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>

#include "layer/ColourMapper.h"
#include "base/AudioLevel.h"

#include <iostream>
#include <cmath>
#include <cassert>

using std::cerr;
using std::endl;

static const int maxLevel = 4; // min is 0, may be mute or not depending on m_includeMute
static const int maxPan = 2; // range is -maxPan to maxPan

LevelPanWidget::LevelPanWidget(QWidget *parent) :
    QWidget(parent),
    m_level(maxLevel),
    m_pan(0),
    m_editable(true),
    m_includeMute(true)
{
}

LevelPanWidget::~LevelPanWidget()
{
}

QSize
LevelPanWidget::sizeHint() const
{
    static double ratio = 0.0;
    if (ratio == 0.0) {
        double baseEm;
#ifdef Q_OS_MAC
        baseEm = 17.0;
#else
        baseEm = 15.0;
#endif
        double em = QFontMetrics(QFont()).height();
        ratio = em / baseEm;
    }

    int pixels = 40;
    int scaled = int(pixels * ratio + 0.5);
    if (pixels != 0 && scaled == 0) scaled = 1;
    return QSize(scaled, scaled);
}

static int
db_to_level(double db)
{
    // Only if !m_includeMute, otherwise AudioLevel is used.
    // Levels are: +6 0 -6 -12 -20
    assert(maxLevel == 4);
    if (db > 3.) return 4;
    else if (db > -3.) return 3;
    else if (db > -9.) return 2;
    else if (db > -16.) return 1;
    else return 0;
}

static double
level_to_db(int level)
{
    // Only if !m_includeMute, otherwise AudioLevel is used.
    // Levels are: +6 0 -6 -12 -20
    assert(maxLevel == 4);
    if (level >= 4) return 6.;
    else if (level == 3) return 0.;
    else if (level == 2) return -6.;
    else if (level == 1) return -12.;
    else return -20.;
}

void
LevelPanWidget::setLevel(float flevel)
{
    int level;
    if (m_includeMute) {
        level = AudioLevel::multiplier_to_fader
            (flevel, maxLevel, AudioLevel::ShortFader);
    } else {
        level = db_to_level(AudioLevel::multiplier_to_dB(flevel));
    }
    if (level < 0) level = 0;
    if (level > maxLevel) level = maxLevel;
    if (level != m_level) {
	m_level = level;
	float convertsTo = getLevel();
	if (fabsf(convertsTo - flevel) > 1e-5) {
	    emitLevelChanged();
	}
	update();
    }
}

float
LevelPanWidget::getLevel() const
{
    if (m_includeMute) {
        return float(AudioLevel::fader_to_multiplier
                     (m_level, maxLevel, AudioLevel::ShortFader));
    } else {
        return float(AudioLevel::dB_to_multiplier(level_to_db(m_level)));
    }
}

void
LevelPanWidget::setPan(float pan)
{
    m_pan = int(round(pan * maxPan));
    if (m_pan < -maxPan) m_pan = -maxPan;
    if (m_pan > maxPan) m_pan = maxPan;
    update();
}

bool
LevelPanWidget::isEditable() const
{
    return m_editable;
}

bool
LevelPanWidget::includesMute() const
{
    return m_includeMute;
}

void
LevelPanWidget::setEditable(bool editable)
{
    m_editable = editable;
    update();
}

void
LevelPanWidget::setIncludeMute(bool include)
{
    m_includeMute = include;
    emitLevelChanged();
    update();
}

float
LevelPanWidget::getPan() const
{
    return float(m_pan) / float(maxPan);
}

void
LevelPanWidget::emitLevelChanged()
{
    cerr << "emitting levelChanged(" << getLevel() << ")" << endl;
    emit levelChanged(getLevel());
}

void
LevelPanWidget::emitPanChanged()
{
    cerr << "emitting panChanged(" << getPan() << ")" << endl;
    emit panChanged(getPan());
}

void
LevelPanWidget::mousePressEvent(QMouseEvent *e)
{
    mouseMoveEvent(e);
}

void
LevelPanWidget::mouseMoveEvent(QMouseEvent *e)
{
    if (!m_editable) return;
    
    int level, pan;
    toCell(rect(), e->pos(), level, pan);
    if (level == m_level && pan == m_pan) {
	return;
    }
    if (level != m_level) {
	m_level = level;
	emitLevelChanged();
    }
    if (pan != m_pan) {
	m_pan = pan;
	emitPanChanged();
    }
    update();
}

void
LevelPanWidget::mouseReleaseEvent(QMouseEvent *e)
{
    mouseMoveEvent(e);
}

void
LevelPanWidget::wheelEvent(QWheelEvent *e)
{
    if (e->modifiers() & Qt::ControlModifier) {
	if (e->delta() > 0) {
	    if (m_pan < maxPan) {
		++m_pan;
		emitPanChanged();
		update();
	    }
	} else {
	    if (m_pan > -maxPan) {
		--m_pan;
		emitPanChanged();
		update();
	    }
	}
    } else {
	if (e->delta() > 0) {
	    if (m_level < maxLevel) {
		++m_level;
		emitLevelChanged();
		update();
	    }
	} else {
	    if (m_level > 0) {
		--m_level;
		emitLevelChanged();
		update();
	    }
	}
    }
}

void
LevelPanWidget::toCell(QRectF rect, QPointF loc, int &level, int &pan) const
{
    double w = rect.width(), h = rect.height();

    int npan = maxPan * 2 + 1;
    int nlevel = maxLevel + 1;

    double wcell = w / npan, hcell = h / nlevel;

    level = int((h - (loc.y() - rect.y())) / hcell);
    if (level < 0) level = 0;
    if (level > maxLevel) level = maxLevel;

    pan = int((loc.x() - rect.x()) / wcell) - maxPan;
    if (pan < -maxPan) pan = -maxPan;
    if (pan > maxPan) pan = maxPan;
}

QSizeF
LevelPanWidget::cellSize(QRectF rect) const
{
    double w = rect.width(), h = rect.height();
    int npan = maxPan * 2 + 1;
    int nlevel = maxLevel + 1;
    double wcell = w / npan, hcell = h / nlevel;
    return QSizeF(wcell, hcell);
}

QPointF
LevelPanWidget::cellCentre(QRectF rect, int level, int pan) const
{
    QSizeF cs = cellSize(rect);
    return QPointF(rect.x() + cs.width() * (pan + maxPan) + cs.width() / 2.,
		   rect.y() + rect.height() - cs.height() * (level + 1) + cs.height() / 2.);
}

QSizeF
LevelPanWidget::cellLightSize(QRectF rect) const
{
    double extent = 3. / 4.;
    QSizeF cs = cellSize(rect);
    double m = std::min(cs.width(), cs.height());
    return QSizeF(m * extent, m * extent);
}

QRectF
LevelPanWidget::cellLightRect(QRectF rect, int level, int pan) const
{
    QSizeF cls = cellLightSize(rect);
    QPointF cc = cellCentre(rect, level, pan);
    return QRectF(cc.x() - cls.width() / 2., 
		  cc.y() - cls.height() / 2.,
		  cls.width(),
		  cls.height());
}

double
LevelPanWidget::thinLineWidth(QRectF rect) const
{
    double tw = ceil(rect.width() / (maxPan * 2. * 10.));
    double th = ceil(rect.height() / (maxLevel * 10.));
    return std::min(th, tw);
}

static QColor
level_to_colour(int level)
{
    assert(maxLevel == 4);
    if (level == 0) return Qt::black;
    else if (level == 1) return QColor(80, 0, 0);
    else if (level == 2) return QColor(160, 0, 0);
    else if (level == 3) return QColor(255, 0, 0);
    else return QColor(255, 255, 0);
}

void
LevelPanWidget::renderTo(QPaintDevice *dev, QRectF rect, bool asIfEditable) const
{
    QPainter paint(dev);

    paint.setRenderHint(QPainter::Antialiasing, true);

    QPen pen;

    double thin = thinLineWidth(rect);

    pen.setColor(QColor(127, 127, 127, 127));
    pen.setWidthF(cellLightSize(rect).width() + thin);
    pen.setCapStyle(Qt::RoundCap);
    paint.setPen(pen);

    for (int pan = -maxPan; pan <= maxPan; ++pan) {
	paint.drawLine(cellCentre(rect, 0, pan), cellCentre(rect, maxLevel, pan));
    }

    if (isEnabled()) {
	pen.setColor(Qt::black);
    } else {
	pen.setColor(Qt::darkGray);
    }

    if (!asIfEditable && m_includeMute && m_level == 0) {
        pen.setWidthF(thin * 2);
        pen.setCapStyle(Qt::RoundCap);
        paint.setPen(pen);
        paint.drawLine(cellCentre(rect, 0, -maxPan),
                       cellCentre(rect, maxLevel, maxPan));
        paint.drawLine(cellCentre(rect, maxLevel, -maxPan),
                       cellCentre(rect, 0, maxPan));
        return;
    }
    
    pen.setWidthF(thin);
    pen.setCapStyle(Qt::FlatCap);
    paint.setPen(pen);
    
    for (int level = 0; level <= m_level; ++level) {
	if (isEnabled()) {
	    paint.setBrush(level_to_colour(level));
	}
	QRectF clr = cellLightRect(rect, level, m_pan);
	if (m_includeMute && m_level == 0) {
	    paint.drawLine(clr.topLeft(), clr.bottomRight());
	    paint.drawLine(clr.bottomLeft(), clr.topRight());
	} else {
	    paint.drawEllipse(clr);
	}
    }
}

void
LevelPanWidget::paintEvent(QPaintEvent *)
{
    renderTo(this, rect(), m_editable);
}