Chris@58: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@0: 
Chris@0: /*
Chris@59:     Sonic Visualiser
Chris@59:     An audio file viewer and annotation editor.
Chris@59:     Centre for Digital Music, Queen Mary, University of London.
Chris@0:     
Chris@59:     This program is free software; you can redistribute it and/or
Chris@59:     modify it under the terms of the GNU General Public License as
Chris@59:     published by the Free Software Foundation; either version 2 of the
Chris@59:     License, or (at your option) any later version.  See the file
Chris@59:     COPYING included with this distribution for more information.
Chris@0: */
Chris@0: 
Chris@0: /**
Chris@0:  * A rotary dial widget.
Chris@0:  *
Chris@0:  * Based on an original design by Thorsten Wilms.
Chris@0:  *
Chris@0:  * Implemented as a widget for the Rosegarden MIDI and audio sequencer
Chris@0:  * and notation editor by Chris Cannam.
Chris@0:  *
Chris@0:  * Extracted into a standalone Qt3 widget by Pedro Lopez-Cabanillas
Chris@0:  * and adapted for use in QSynth.
Chris@0:  * 
Chris@0:  * Ported to Qt4 by Chris Cannam.
Chris@0:  *
Chris@168:  * This file copyright 2003-2006 Chris Cannam, copyright 2005 Pedro
Chris@182:  * Lopez-Cabanillas, copyright 2006 Queen Mary, University of London.
Chris@0:  *
Chris@0:  * This program is free software; you can redistribute it and/or
Chris@0:  * modify it under the terms of the GNU General Public License as
Chris@0:  * published by the Free Software Foundation; either version 2 of the
Chris@0:  * License, or (at your option) any later version.  See the file
Chris@0:  * COPYING included with this distribution for more information.
Chris@0:  */
Chris@0: 
Chris@0: #include "AudioDial.h"
Chris@0: 
Chris@167: #include "base/RangeMapper.h"
Chris@167: 
Chris@0: #include <cmath>
Chris@0: #include <iostream>
Chris@0: 
Chris@0: #include <QTimer>
Chris@0: #include <QPainter>
Chris@0: #include <QPixmap>
Chris@0: #include <QColormap>
Chris@0: #include <QMouseEvent>
Chris@0: #include <QPaintEvent>
Chris@34: #include <QInputDialog>
Chris@0: 
Chris@0: using std::endl;
Chris@0: using std::cerr;
Chris@0: 
Chris@0: 
Chris@0: //!!! Pedro updated his version to use my up/down response code from RG -- need to grab that code in preference to this version from Rui
Chris@0: 
Chris@0: 
Chris@0: //-------------------------------------------------------------------------
Chris@0: // AudioDial - Instance knob widget class.
Chris@0: //
Chris@0: 
Chris@0: #define AUDIO_DIAL_MIN (0.25 * M_PI)
Chris@0: #define AUDIO_DIAL_MAX (1.75 * M_PI)
Chris@0: #define AUDIO_DIAL_RANGE (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN)
Chris@0: 
Chris@0: 
Chris@212: //static int dialsExtant = 0;
Chris@189: 
Chris@189: 
Chris@0: // Constructor.
Chris@0: AudioDial::AudioDial(QWidget *parent) :
Chris@0:     QDial(parent),
Chris@170:     m_knobColor(Qt::black),
Chris@170:     m_meterColor(Qt::white),
Chris@167:     m_defaultValue(0),
Chris@168:     m_mappedValue(0),
Chris@168:     m_noMappedUpdate(false),
Chris@187:     m_showTooltip(true),
Chris@167:     m_rangeMapper(0)
Chris@0: {
Chris@0:     m_mouseDial = false;
Chris@0:     m_mousePressed = false;
Chris@212: //    ++dialsExtant;
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: // Destructor.
Chris@0: AudioDial::~AudioDial (void)
Chris@0: {
Chris@167:     delete m_rangeMapper;
Chris@212: //    --dialsExtant;
Chris@167: }
Chris@167: 
Chris@167: 
Chris@167: void AudioDial::setRangeMapper(RangeMapper *mapper)
Chris@167: {
Chris@212: //    std::cerr << "AudioDial[" << this << "][\"" << objectName().toStdString() << "\"::setRangeMapper(" << mapper << ") [current is " << m_rangeMapper << "] (have " << dialsExtant << " dials extant)" << std::endl;
Chris@189: 
Chris@187:     if (m_rangeMapper == mapper) return;
Chris@187: 
Chris@170:     if (!m_rangeMapper && mapper) {
Chris@168:         connect(this, SIGNAL(valueChanged(int)),
Chris@168:                 this, SLOT(updateMappedValue(int)));
Chris@168:     }
Chris@170: 
Chris@167:     delete m_rangeMapper;
Chris@167:     m_rangeMapper = mapper;
Chris@170: 
Chris@187:     updateMappedValue(value());
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: void AudioDial::paintEvent(QPaintEvent *)
Chris@0: {
Chris@0:     QPainter paint;
Chris@0: 
Chris@0:     float angle = AUDIO_DIAL_MIN // offset
Chris@0: 	+ (AUDIO_DIAL_RANGE *
Chris@0: 	   (float(QDial::value() - QDial::minimum()) /
Chris@0: 	    (float(QDial::maximum() - QDial::minimum()))));
Chris@0:     int degrees = int(angle * 180.0 / M_PI);
Chris@0: 
Chris@0:     int ns = notchSize();
Chris@0:     int numTicks = 1 + (maximum() + ns - minimum()) / ns;
Chris@0: 	
Chris@0:     QColor knobColor(m_knobColor);
Chris@0:     if (knobColor == Qt::black)
Chris@82: 	knobColor = palette().background().color();
Chris@0: 
Chris@0:     QColor meterColor(m_meterColor);
Chris@0:     if (!isEnabled())
Chris@0: 	meterColor = palette().mid().color();
Chris@0:     else if (m_meterColor == Qt::white)
Chris@0: 	meterColor = palette().highlight().color();
Chris@0: 
Chris@0:     int m_size = width() < height() ? width() : height();
Chris@0:     int scale = 1;
Chris@0:     int width = m_size - 2*scale, height = m_size - 2*scale;
Chris@0: 
Chris@0:     paint.begin(this);
Chris@0:     paint.setRenderHint(QPainter::Antialiasing, true);
Chris@0:     paint.translate(1, 1);
Chris@0: 
Chris@0:     QPen pen;
Chris@0:     QColor c;
Chris@0: 
Chris@0:     // Knob body and face...
Chris@0: 
Chris@0:     c = knobColor;
Chris@0:     pen.setColor(knobColor);
Chris@0:     pen.setWidth(scale * 2);
Chris@0:     pen.setCapStyle(Qt::FlatCap);
Chris@0: 	
Chris@0:     paint.setPen(pen);
Chris@0:     paint.setBrush(c);
Chris@0: 
Chris@0:     int indent = (int)(width * 0.15 + 1);
Chris@0: 
Chris@0:     paint.drawEllipse(indent-1, indent-1, width-2*indent, width-2*indent);
Chris@0: 
Chris@0:     pen.setWidth(3 * scale);
Chris@0:     int pos = indent-1 + (width-2*indent) / 20;
Chris@0:     int darkWidth = (width-2*indent) * 3 / 4;
Chris@0:     while (darkWidth) {
Chris@0: 	c = c.light(102);
Chris@0: 	pen.setColor(c);
Chris@0: 	paint.setPen(pen);
Chris@0: 	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
Chris@0: 	if (!--darkWidth) break;
Chris@0: 	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
Chris@0: 	if (!--darkWidth) break;
Chris@0: 	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
Chris@0: 	++pos; --darkWidth;
Chris@0:     }
Chris@0: 
Chris@0:     // Tick notches...
Chris@0: 
Chris@34:     if ( notchesVisible() ) {
Chris@0: //	std::cerr << "Notches visible" << std::endl;
Chris@0: 	pen.setColor(palette().dark().color());
Chris@0: 	pen.setWidth(scale);
Chris@0: 	paint.setPen(pen);
Chris@0: 	for (int i = 0; i < numTicks; ++i) {
Chris@0: 	    int div = numTicks;
Chris@0: 	    if (div > 1) --div;
Chris@0: 	    drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
Chris@0: 		     width, true);
Chris@0: 	}
Chris@0:     }
Chris@0: 
Chris@0:     // The bright metering bit...
Chris@0: 
Chris@0:     c = meterColor;
Chris@0:     pen.setColor(c);
Chris@0:     pen.setWidth(indent);
Chris@0:     paint.setPen(pen);
Chris@0: 
Chris@0: //    std::cerr << "degrees " << degrees << ", gives us " << -(degrees - 45) * 16 << std::endl;
Chris@0: 
Chris@0:     int arcLen = -(degrees - 45) * 16;
Chris@0:     if (arcLen == 0) arcLen = -16;
Chris@0: 
Chris@0:     paint.drawArc(indent/2, indent/2,
Chris@0: 		  width-indent, width-indent, (180 + 45) * 16, arcLen);
Chris@0: 
Chris@0:     paint.setBrush(Qt::NoBrush);
Chris@0: 
Chris@0:     // Shadowing...
Chris@0: 
Chris@0:     pen.setWidth(scale);
Chris@0:     paint.setPen(pen);
Chris@0: 
Chris@0:     // Knob shadow...
Chris@0: 
Chris@0:     int shadowAngle = -720;
Chris@0:     c = knobColor.dark();
Chris@0:     for (int arc = 120; arc < 2880; arc += 240) {
Chris@0: 	pen.setColor(c);
Chris@0: 	paint.setPen(pen);
Chris@0: 	paint.drawArc(indent, indent,
Chris@0: 		      width-2*indent, width-2*indent, shadowAngle + arc, 240);
Chris@0: 	paint.drawArc(indent, indent,
Chris@0: 		      width-2*indent, width-2*indent, shadowAngle - arc, 240);
Chris@0: 	c = c.light(110);
Chris@0:     }
Chris@0: 
Chris@0:     // Scale shadow...
Chris@0: 
Chris@0:     shadowAngle = 2160;
Chris@0:     c = palette().dark().color();
Chris@0:     for (int arc = 120; arc < 2880; arc += 240) {
Chris@0: 	pen.setColor(c);
Chris@0: 	paint.setPen(pen);
Chris@0: 	paint.drawArc(scale/2, scale/2,
Chris@0: 		      width-scale, width-scale, shadowAngle + arc, 240);
Chris@0: 	paint.drawArc(scale/2, scale/2,
Chris@0: 		      width-scale, width-scale, shadowAngle - arc, 240);
Chris@0: 	c = c.light(108);
Chris@0:     }
Chris@0: 
Chris@0:     // Undraw the bottom part...
Chris@0: 
Chris@0:     pen.setColor(palette().background().color());
Chris@0:     pen.setWidth(scale * 4);
Chris@0:     paint.setPen(pen);
Chris@0:     paint.drawArc(scale/2, scale/2,
Chris@0: 		  width-scale, width-scale, -45 * 16, -92 * 16);
Chris@0: 
Chris@0:     // Scale ends...
Chris@0: 
Chris@0:     pen.setColor(palette().dark().color());
Chris@0:     pen.setWidth(scale);
Chris@0:     paint.setPen(pen);
Chris@0:     for (int i = 0; i < numTicks; ++i) {
Chris@0: 	if (i != 0 && i != numTicks - 1) continue;
Chris@0: 	int div = numTicks;
Chris@0: 	if (div > 1) --div;
Chris@0: 	drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
Chris@0: 		 width, false);
Chris@0:     }
Chris@0: 
Chris@0:     // Pointer notch...
Chris@0: 
Chris@0:     float hyp = float(width) / 2.0;
Chris@0:     float len = hyp - indent;
Chris@0:     --len;
Chris@0: 
Chris@0:     float x0 = hyp;
Chris@0:     float y0 = hyp;
Chris@0: 
Chris@0:     float x = hyp - len * sin(angle);
Chris@0:     float y = hyp + len * cos(angle);
Chris@0: 
Chris@0:     c = palette().dark().color();
Chris@0:     pen.setColor(isEnabled() ? c.dark(130) : c);
Chris@0:     pen.setWidth(scale * 2);
Chris@0:     paint.setPen(pen);
Chris@0:     paint.drawLine(int(x0), int(y0), int(x), int(y));
Chris@0: 
Chris@0:     paint.end();
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: void AudioDial::drawTick(QPainter &paint,
Chris@0: 			 float angle, int size, bool internal)
Chris@0: {
Chris@0:     float hyp = float(size) / 2.0;
Chris@0:     float x0 = hyp - (hyp - 1) * sin(angle);
Chris@0:     float y0 = hyp + (hyp - 1) * cos(angle);
Chris@0: 
Chris@0: //    cerr << "drawTick: angle " << angle << ", size " << size << ", internal " << internal << endl;
Chris@0:     
Chris@0:     if (internal) {
Chris@0: 
Chris@0: 	float len = hyp / 4;
Chris@0: 	float x1 = hyp - (hyp - len) * sin(angle);
Chris@0: 	float y1 = hyp + (hyp - len) * cos(angle);
Chris@0: 		
Chris@0: 	paint.drawLine(int(x0), int(y0), int(x1), int(y1));
Chris@0: 
Chris@0:     } else {
Chris@0: 
Chris@0: 	float len = hyp / 4;
Chris@0: 	float x1 = hyp - (hyp + len) * sin(angle);
Chris@0: 	float y1 = hyp + (hyp + len) * cos(angle);
Chris@0: 
Chris@0: 	paint.drawLine(int(x0), int(y0), int(x1), int(y1));
Chris@0:     }
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: void AudioDial::setKnobColor(const QColor& color)
Chris@0: {
Chris@0:     m_knobColor = color;
Chris@0:     update();
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: void AudioDial::setMeterColor(const QColor& color)
Chris@0: {
Chris@0:     m_meterColor = color;
Chris@0:     update();
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: void AudioDial::setMouseDial(bool mouseDial)
Chris@0: {
Chris@0:     m_mouseDial = mouseDial;
Chris@0: }
Chris@0: 
Chris@0: 
Chris@34: void AudioDial::setDefaultValue(int defaultValue)
Chris@34: {
Chris@34:     m_defaultValue = defaultValue;
Chris@34: }
Chris@34: 
Chris@34: 
Chris@219: void AudioDial::setValue(int value)
Chris@219: {
Chris@219:     QDial::setValue(value);
Chris@219:     updateMappedValue(value);
Chris@219: }
Chris@219: 
Chris@219: 
Chris@177: void AudioDial::setMappedValue(float mappedValue)
Chris@177: {
Chris@177:     if (m_rangeMapper) {
Chris@177:         int newPosition = m_rangeMapper->getPositionForValue(mappedValue);
Chris@190:         bool changed = (m_mappedValue != mappedValue);
Chris@177:         m_mappedValue = mappedValue;
Chris@177:         m_noMappedUpdate = true;
Chris@177:         std::cerr << "AudioDial::setMappedValue(" << mappedValue << "): new position is " << newPosition << std::endl;
Chris@177:         if (newPosition != value()) {
Chris@177:             setValue(newPosition);
Chris@190:         } else if (changed) {
Chris@177:             emit valueChanged(newPosition);
Chris@177:         }
Chris@177:         m_noMappedUpdate = false;
Chris@177:     } else {
Chris@187:         setValue(int(mappedValue));
Chris@177:     }
Chris@177: }
Chris@177: 
Chris@177: 
Chris@168: void AudioDial::setShowToolTip(bool show)
Chris@168: {
Chris@168:     m_showTooltip = show;
Chris@168:     m_noMappedUpdate = true;
Chris@168:     updateMappedValue(value());
Chris@168:     m_noMappedUpdate = false;
Chris@168: }
Chris@168: 
Chris@168: 
Chris@167: float AudioDial::mappedValue() const
Chris@167: {
Chris@168:     if (m_rangeMapper) {
Chris@168:         std::cerr << "AudioDial::mappedValue(): value = " << value() << ", mappedValue = " << m_mappedValue << std::endl;
Chris@168:         return m_mappedValue;
Chris@168:     }
Chris@168:     return value();
Chris@168: }
Chris@168: 
Chris@168: 
Chris@168: void AudioDial::updateMappedValue(int value)
Chris@168: {
Chris@170:     if (!m_noMappedUpdate) {
Chris@170:         if (m_rangeMapper) {
Chris@168:             m_mappedValue = m_rangeMapper->getValueForPosition(value);
Chris@170:         } else {
Chris@170:             m_mappedValue = value;
Chris@168:         }
Chris@168:     }
Chris@168: 
Chris@168:     if (m_showTooltip) {
Chris@168:         QString name = objectName();
Chris@168:         QString unit = "";
Chris@168:         QString text;
Chris@168:         if (m_rangeMapper) unit = m_rangeMapper->getUnit();
Chris@168:         if (name != "") {
Chris@168:             text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
Chris@168:         } else {
Chris@168:             text = tr("%2%3").arg(m_mappedValue).arg(unit);
Chris@168:         }
Chris@168:         setToolTip(text);
Chris@168:     }
Chris@167: }
Chris@167: 
Chris@167: 
Chris@0: // Alternate mouse behavior event handlers.
Chris@0: void AudioDial::mousePressEvent(QMouseEvent *mouseEvent)
Chris@0: {
Chris@0:     if (m_mouseDial) {
Chris@0: 	QDial::mousePressEvent(mouseEvent);
Chris@187:     } else if (mouseEvent->button() == Qt::MidButton ||
Chris@187:                ((mouseEvent->button() == Qt::LeftButton) &&
Chris@187:                 (mouseEvent->modifiers() & Qt::ControlModifier))) {
Chris@34: 	int dv = m_defaultValue;
Chris@34: 	if (dv < minimum()) dv = minimum();
Chris@34: 	if (dv > maximum()) dv = maximum();
Chris@34: 	setValue(m_defaultValue);
Chris@187:     } else if (mouseEvent->button() == Qt::LeftButton) {
Chris@187: 	m_mousePressed = true;
Chris@187: 	m_posMouse = mouseEvent->pos();
Chris@34:     }
Chris@34: }
Chris@34: 
Chris@34: 
Chris@34: void AudioDial::mouseDoubleClickEvent(QMouseEvent *mouseEvent)
Chris@34: {
Chris@187:     //!!! needs a common base class with Thumbwheel
Chris@187: 
Chris@34:     if (m_mouseDial) {
Chris@34: 	QDial::mouseDoubleClickEvent(mouseEvent);
Chris@187:     } else if (mouseEvent->button() != Qt::LeftButton) {
Chris@187:         return;
Chris@187:     }
Chris@167: 
Chris@187:     bool ok = false;
Chris@167: 
Chris@187:     if (m_rangeMapper) {
Chris@187:         
Chris@187:         float min = m_rangeMapper->getValueForPosition(minimum());
Chris@187:         float max = m_rangeMapper->getValueForPosition(maximum());
Chris@187:         
Chris@187:         if (min > max) { 
Chris@187:             float tmp = min;
Chris@187:             min = max;
Chris@187:             max = tmp;
Chris@187:         }
Chris@167: 
Chris@187:         QString unit = m_rangeMapper->getUnit();
Chris@187:         
Chris@187:         QString text;
Chris@187:         if (objectName() != "") {
Chris@187:             if (unit != "") {
Chris@187:                 text = tr("New value for %1, from %2 to %3 %4:")
Chris@187:                     .arg(objectName()).arg(min).arg(max).arg(unit);
Chris@167:             } else {
Chris@187:                 text = tr("New value for %1, from %2 to %3:")
Chris@187:                     .arg(objectName()).arg(min).arg(max);
Chris@167:             }
Chris@187:         } else {
Chris@187:             if (unit != "") {
Chris@187:                 text = tr("Enter a new value from %1 to %2 %3:")
Chris@187:                     .arg(min).arg(max).arg(unit);
Chris@187:             } else {
Chris@187:                 text = tr("Enter a new value from %1 to %2:")
Chris@187:                     .arg(min).arg(max);
Chris@168:             }
Chris@187:         }
Chris@187:         
Chris@187:         float newValue = QInputDialog::getDouble
Chris@187:             (this,
Chris@187:              tr("Enter new value"),
Chris@187:              text,
Chris@187:              m_mappedValue,
Chris@187:              min,
Chris@187:              max,
Chris@187:              4, 
Chris@187:              &ok);
Chris@187:         
Chris@187:         if (ok) {
Chris@187:             setMappedValue(newValue);
Chris@187:         }
Chris@187:         
Chris@187:     } else {
Chris@187:         
Chris@187:         int newPosition = QInputDialog::getInteger
Chris@187:             (this,
Chris@187:              tr("Enter new value"),
Chris@187:              tr("Enter a new value from %1 to %2:")
Chris@187:              .arg(minimum()).arg(maximum()),
Chris@187:              value(), minimum(), maximum(), pageStep(), &ok);
Chris@187:         
Chris@187:         if (ok) {
Chris@187:             setValue(newPosition);
Chris@167:         }
Chris@0:     }
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: void AudioDial::mouseMoveEvent(QMouseEvent *mouseEvent)
Chris@0: {
Chris@0:     if (m_mouseDial) {
Chris@0: 	QDial::mouseMoveEvent(mouseEvent);
Chris@0:     } else if (m_mousePressed) {
Chris@0: 	const QPoint& posMouse = mouseEvent->pos();
Chris@0: 	int v = QDial::value()
Chris@0: 	    + (posMouse.x() - m_posMouse.x())
Chris@0: 	    + (m_posMouse.y() - posMouse.y());
Chris@0: 	if (v > QDial::maximum())
Chris@0: 	    v = QDial::maximum();
Chris@0: 	else
Chris@0: 	    if (v < QDial::minimum())
Chris@0: 		v = QDial::minimum();
Chris@0: 	m_posMouse = posMouse;
Chris@0: 	QDial::setValue(v);
Chris@0:     }
Chris@0: }
Chris@0: 
Chris@0: 
Chris@0: void AudioDial::mouseReleaseEvent(QMouseEvent *mouseEvent)
Chris@0: {
Chris@0:     if (m_mouseDial) {
Chris@0: 	QDial::mouseReleaseEvent(mouseEvent);
Chris@0:     } else if (m_mousePressed) {
Chris@0: 	m_mousePressed = false;
Chris@0:     }
Chris@0: }
Chris@0: 
Chris@189: void
Chris@189: AudioDial::enterEvent(QEvent *e)
Chris@189: {
Chris@189:     QDial::enterEvent(e);
Chris@189:     emit mouseEntered();
Chris@189: }
Chris@189: 
Chris@189: void
Chris@189: AudioDial::leaveEvent(QEvent *e)
Chris@189: {
Chris@189:     QDial::enterEvent(e);
Chris@189:     emit mouseLeft();
Chris@189: }
Chris@189: