Chris@132: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@132: 
Chris@132: /*
Chris@132:     Sonic Visualiser
Chris@132:     An audio file viewer and annotation editor.
Chris@132:     Centre for Digital Music, Queen Mary, University of London.
Chris@182:     This file copyright 2006 QMUL.
Chris@132: 
Chris@132:     This program is free software; you can redistribute it and/or
Chris@132:     modify it under the terms of the GNU General Public License as
Chris@132:     published by the Free Software Foundation; either version 2 of the
Chris@132:     License, or (at your option) any later version.  See the file
Chris@132:     COPYING included with this distribution for more information.
Chris@132: */
Chris@132: 
Chris@132: #include "Thumbwheel.h"
Chris@132: 
Chris@187: #include "base/RangeMapper.h"
Chris@190: #include "base/Profiler.h"
Chris@187: 
Chris@132: #include <QMouseEvent>
Chris@132: #include <QPaintEvent>
Chris@132: #include <QWheelEvent>
Chris@187: #include <QInputDialog>
Chris@132: #include <QPainter>
Chris@190: #include <QPainterPath>
Chris@132: 
Chris@132: #include <cmath>
Chris@132: #include <iostream>
Chris@132: 
Chris@133: Thumbwheel::Thumbwheel(Qt::Orientation orientation,
Chris@132: 		       QWidget *parent) :
Chris@132:     QWidget(parent),
Chris@133:     m_min(0),
Chris@133:     m_max(100),
Chris@133:     m_default(50),
Chris@133:     m_value(50),
Chris@187:     m_mappedValue(50),
Chris@187:     m_noMappedUpdate(false),
Chris@165:     m_rotation(0.5),
Chris@132:     m_orientation(orientation),
Chris@165:     m_speed(1.0),
Chris@132:     m_tracking(true),
Chris@132:     m_showScale(true),
Chris@132:     m_clicked(false),
Chris@133:     m_atDefault(true),
Chris@187:     m_clickRotation(m_rotation),
Chris@187:     m_showTooltip(true),
Chris@187:     m_rangeMapper(0)
Chris@132: {
Chris@132: }
Chris@132: 
Chris@132: Thumbwheel::~Thumbwheel()
Chris@132: {
Chris@187:     delete m_rangeMapper;
Chris@187: }
Chris@187: 
Chris@187: void
Chris@187: Thumbwheel::setRangeMapper(RangeMapper *mapper)
Chris@187: {
Chris@187:     if (m_rangeMapper == mapper) return;
Chris@187: 
Chris@187:     if (!m_rangeMapper && mapper) {
Chris@187:         connect(this, SIGNAL(valueChanged(int)),
Chris@187:                 this, SLOT(updateMappedValue(int)));
Chris@187:     }
Chris@187: 
Chris@187:     delete m_rangeMapper;
Chris@187:     m_rangeMapper = mapper;
Chris@187: 
Chris@187:     updateMappedValue(getValue());
Chris@187: }
Chris@187: 
Chris@187: void
Chris@187: Thumbwheel::setShowToolTip(bool show)
Chris@187: {
Chris@187:     m_showTooltip = show;
Chris@187:     m_noMappedUpdate = true;
Chris@187:     updateMappedValue(getValue());
Chris@187:     m_noMappedUpdate = false;
Chris@132: }
Chris@132: 
Chris@132: void
Chris@133: Thumbwheel::setMinimumValue(int min)
Chris@133: {
Chris@133:     if (m_min == min) return;
Chris@133: 
Chris@133:     m_min = min;
Chris@133:     if (m_max <= m_min) m_max = m_min + 1;
Chris@133:     if (m_value < m_min) m_value = m_min;
Chris@133:     if (m_value > m_max) m_value = m_max;
Chris@165: 
Chris@165:     m_rotation = float(m_value - m_min) / float(m_max - m_min);
Chris@165:     update();
Chris@133: }
Chris@133: 
Chris@133: int
Chris@133: Thumbwheel::getMinimumValue() const
Chris@133: {
Chris@133:     return m_min;
Chris@133: }
Chris@133: 
Chris@133: void
Chris@133: Thumbwheel::setMaximumValue(int max)
Chris@133: {
Chris@133:     if (m_max == max) return;
Chris@133: 
Chris@133:     m_max = max;
Chris@133:     if (m_min >= m_max) m_min = m_max - 1;
Chris@133:     if (m_value < m_min) m_value = m_min;
Chris@133:     if (m_value > m_max) m_value = m_max;
Chris@165: 
Chris@165:     m_rotation = float(m_value - m_min) / float(m_max - m_min);
Chris@165:     update();
Chris@133: }
Chris@133: 
Chris@133: int
Chris@133: Thumbwheel::getMaximumValue() const
Chris@133: {
Chris@133:     return m_max;
Chris@133: }
Chris@133: 
Chris@133: void
Chris@133: Thumbwheel::setDefaultValue(int deft)
Chris@133: {
Chris@133:     if (m_default == deft) return;
Chris@133: 
Chris@133:     m_default = deft;
Chris@133:     if (m_atDefault) {
Chris@133:         setValue(m_default);
Chris@165:         m_atDefault = true; // setValue unsets this
Chris@382:         m_cache = QImage();
Chris@133:         emit valueChanged(getValue());
Chris@133:     }
Chris@133: }
Chris@133: 
Chris@187: void
Chris@908: Thumbwheel::setMappedValue(double mappedValue)
Chris@187: {
Chris@187:     if (m_rangeMapper) {
Chris@187:         int newValue = m_rangeMapper->getPositionForValue(mappedValue);
Chris@190:         bool changed = (m_mappedValue != mappedValue);
Chris@187:         m_mappedValue = mappedValue;
Chris@187:         m_noMappedUpdate = true;
Chris@587: //        SVDEBUG << "Thumbwheel::setMappedValue(" << mappedValue << "): new value is " << newValue << " (visible " << isVisible() << ")" << endl;
Chris@187:         if (newValue != getValue()) {
Chris@187:             setValue(newValue);
Chris@190:             changed = true;
Chris@382:             m_cache = QImage();
Chris@187:         }
Chris@190:         if (changed) emit valueChanged(newValue);
Chris@187:         m_noMappedUpdate = false;
Chris@187:     } else {
Chris@190:         int v = int(mappedValue);
Chris@190:         if (v != getValue()) {
Chris@190:             setValue(v);
Chris@382:             m_cache = QImage();
Chris@190:             emit valueChanged(v);
Chris@190:         }
Chris@187:     }
Chris@187: }
Chris@187: 
Chris@133: int
Chris@133: Thumbwheel::getDefaultValue() const
Chris@133: {
Chris@133:     return m_default;
Chris@133: }
Chris@133: 
Chris@133: void
Chris@132: Thumbwheel::setValue(int value)
Chris@132: {
Chris@587: //    SVDEBUG << "Thumbwheel::setValue(" << value << ") (from " << m_value
Chris@585: //              << ", rotation " << m_rotation << ")" << " (visible " << isVisible() << ")" << endl;
Chris@133: 
Chris@165:     if (m_value != value) {
Chris@165: 
Chris@165:         m_atDefault = false;
Chris@165: 
Chris@165:         if (value < m_min) value = m_min;
Chris@165:         if (value > m_max) value = m_max;
Chris@165:         m_value = value;
Chris@165:     }
Chris@165: 
Chris@165:     m_rotation = float(m_value - m_min) / float(m_max - m_min);
Chris@382:     m_cache = QImage();
Chris@192:     if (isVisible()) update();
Chris@132: }
Chris@132: 
Chris@133: void
Chris@133: Thumbwheel::resetToDefault()
Chris@133: {
Chris@133:     if (m_default == m_value) return;
Chris@133:     setValue(m_default);
Chris@133:     m_atDefault = true;
Chris@382:     m_cache = QImage();
Chris@133:     emit valueChanged(getValue());
Chris@133: }
Chris@133: 
Chris@132: int
Chris@132: Thumbwheel::getValue() const
Chris@132: {
Chris@132:     return m_value;
Chris@132: }
Chris@132: 
Chris@908: double
Chris@187: Thumbwheel::getMappedValue() const
Chris@187: {
Chris@187:     if (m_rangeMapper) {
Chris@587: //        SVDEBUG << "Thumbwheel::getMappedValue(): value = " << getValue() << ", mappedValue = " << m_mappedValue << endl;
Chris@187:         return m_mappedValue;
Chris@187:     }
Chris@187:     return getValue();
Chris@187: }
Chris@187: 
Chris@187: void
Chris@187: Thumbwheel::updateMappedValue(int value)
Chris@187: {
Chris@187:     if (!m_noMappedUpdate) {
Chris@187:         if (m_rangeMapper) {
Chris@187:             m_mappedValue = m_rangeMapper->getValueForPosition(value);
Chris@187:         } else {
Chris@187:             m_mappedValue = value;
Chris@187:         }
Chris@187:     }
Chris@187: 
Chris@187:     if (m_showTooltip) {
Chris@187:         QString name = objectName();
Chris@187:         QString unit = "";
Chris@187:         QString text;
Chris@187:         if (m_rangeMapper) unit = m_rangeMapper->getUnit();
Chris@187:         if (name != "") {
Chris@187:             text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
Chris@187:         } else {
Chris@187:             text = tr("%2%3").arg(m_mappedValue).arg(unit);
Chris@187:         }
Chris@187:         setToolTip(text);
Chris@187:     }
Chris@187: }
Chris@187: 
Chris@132: void
Chris@256: Thumbwheel::scroll(bool up)
Chris@256: {
Chris@908:     int step = int(lrintf(m_speed));
Chris@256:     if (step == 0) step = 1;
Chris@256: 
Chris@256:     if (up) {
Chris@256: 	setValue(m_value + step);
Chris@256:     } else {
Chris@256: 	setValue(m_value - step);
Chris@256:     }
Chris@256:     
Chris@256:     emit valueChanged(getValue());
Chris@256: }
Chris@256: 
Chris@256: void
Chris@132: Thumbwheel::setSpeed(float speed)
Chris@132: {
Chris@132:     m_speed = speed;
Chris@132: }
Chris@132: 
Chris@132: float
Chris@132: Thumbwheel::getSpeed() const
Chris@132: {
Chris@132:     return m_speed;
Chris@132: }
Chris@132: 
Chris@132: void
Chris@132: Thumbwheel::setTracking(bool tracking)
Chris@132: {
Chris@132:     m_tracking = tracking;
Chris@132: }
Chris@132: 
Chris@132: bool
Chris@132: Thumbwheel::getTracking() const
Chris@132: {
Chris@132:     return m_tracking;
Chris@132: }
Chris@132: 
Chris@132: void
Chris@132: Thumbwheel::setShowScale(bool showScale)
Chris@132: {
Chris@132:     m_showScale = showScale;
Chris@132: }
Chris@132: 
Chris@132: bool
Chris@132: Thumbwheel::getShowScale() const
Chris@132: {
Chris@132:     return m_showScale;
Chris@132: }
Chris@132: 
Chris@132: void
Chris@189: Thumbwheel::enterEvent(QEvent *)
Chris@189: {
Chris@189:     emit mouseEntered();
Chris@189: }
Chris@189: 
Chris@189: void
Chris@189: Thumbwheel::leaveEvent(QEvent *)
Chris@189: {
Chris@189:     emit mouseLeft();
Chris@189: }
Chris@189: 
Chris@189: void
Chris@132: Thumbwheel::mousePressEvent(QMouseEvent *e)
Chris@132: {
Chris@187:     if (e->button() == Qt::MidButton ||
Chris@187:         ((e->button() == Qt::LeftButton) &&
Chris@187:          (e->modifiers() & Qt::ControlModifier))) {
Chris@187:         resetToDefault();
Chris@187:     } else if (e->button() == Qt::LeftButton) {
Chris@133:         m_clicked = true;
Chris@133:         m_clickPos = e->pos();
Chris@165:         m_clickRotation = m_rotation;
Chris@133:     }
Chris@132: }
Chris@132: 
Chris@132: void
Chris@187: Thumbwheel::mouseDoubleClickEvent(QMouseEvent *mouseEvent)
Chris@132: {
Chris@188:     //!!! needs a common base class with AudioDial (and Panner?)
Chris@187: 
Chris@187:     if (mouseEvent->button() != Qt::LeftButton) {
Chris@187:         return;
Chris@187:     }
Chris@187: 
Chris@187:     bool ok = false;
Chris@187: 
Chris@187:     if (m_rangeMapper) {
Chris@187:         
Chris@908:         double min = m_rangeMapper->getValueForPosition(m_min);
Chris@908:         double max = m_rangeMapper->getValueForPosition(m_max);
Chris@187:                 
Chris@187:         if (min > max) { 
Chris@908:             double tmp = min;
Chris@187:             min = max;
Chris@187:             max = tmp;
Chris@187:         }
Chris@187: 
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@187:             } else {
Chris@187:                 text = tr("New value for %1, from %2 to %3:")
Chris@187:                     .arg(objectName()).arg(min).arg(max);
Chris@187:             }
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@187:             }
Chris@187:         }
Chris@187:         
Chris@908:         double 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@616:         int newValue = QInputDialog::getInt
Chris@187:             (this,
Chris@187:              tr("Enter new value"),
Chris@187:              tr("Enter a new value from %1 to %2:")
Chris@187:              .arg(m_min).arg(m_max),
Chris@187:              getValue(), m_min, m_max, 1, &ok);
Chris@187:         
Chris@187:         if (ok) {
Chris@187:             setValue(newValue);
Chris@187:         }
Chris@187:     }
Chris@132: }
Chris@132: 
Chris@187: 
Chris@132: void
Chris@132: Thumbwheel::mouseMoveEvent(QMouseEvent *e)
Chris@132: {
Chris@133:     if (!m_clicked) return;
Chris@132:     int dist = 0;
Chris@132:     if (m_orientation == Qt::Horizontal) {
Chris@132:         dist = e->x() - m_clickPos.x();
Chris@132:     } else {
Chris@132:         dist = e->y() - m_clickPos.y();
Chris@132:     }
Chris@165: 
Chris@908:     float rotation = m_clickRotation + (m_speed * float(dist)) / 100;
Chris@165:     if (rotation < 0.f) rotation = 0.f;
Chris@165:     if (rotation > 1.f) rotation = 1.f;
Chris@908:     int value = int(lrintf(float(m_min) + float(m_max - m_min) * m_rotation));
Chris@132:     if (value != m_value) {
Chris@132:         setValue(value);
Chris@132:         if (m_tracking) emit valueChanged(getValue());
Chris@165:         m_rotation = rotation;
Chris@165:     } else if (fabsf(rotation - m_rotation) > 0.001) {
Chris@165:         m_rotation = rotation;
Chris@165:         repaint();
Chris@165:     }
Chris@132: }
Chris@132: 
Chris@132: void
Chris@132: Thumbwheel::mouseReleaseEvent(QMouseEvent *e)
Chris@132: {
Chris@133:     if (!m_clicked) return;
Chris@132:     bool reallyTracking = m_tracking;
Chris@132:     m_tracking = true;
Chris@132:     mouseMoveEvent(e);
Chris@132:     m_tracking = reallyTracking;
Chris@133:     m_clicked = false;
Chris@132: }
Chris@132: 
Chris@132: void
Chris@132: Thumbwheel::wheelEvent(QWheelEvent *e)
Chris@132: {
Chris@908:     int step = int(lrintf(m_speed));
Chris@132:     if (step == 0) step = 1;
Chris@132: 
Chris@132:     if (e->delta() > 0) {
Chris@132: 	setValue(m_value + step);
Chris@132:     } else {
Chris@132: 	setValue(m_value - step);
Chris@132:     }
Chris@132:     
Chris@132:     emit valueChanged(getValue());
Chris@132: }
Chris@132: 
Chris@132: void
Chris@132: Thumbwheel::paintEvent(QPaintEvent *)
Chris@132: {
Chris@382:     Profiler profiler("Thumbwheel::paintEvent");
Chris@382: 
Chris@382:     if (!m_cache.isNull()) {
Chris@382:         QPainter paint(this);
Chris@382:         paint.drawImage(0, 0, m_cache);
Chris@382:         return;
Chris@382:     }
Chris@382: 
Chris@382:     Profiler profiler2("Thumbwheel::paintEvent (no cache)");
Chris@382: 
Chris@382:     m_cache = QImage(size(), QImage::Format_ARGB32);
Chris@382:     m_cache.fill(Qt::transparent);
Chris@190: 
Chris@191:     int bw = 3;
Chris@191: 
Chris@191:     QRect subclip;
Chris@191:     if (m_orientation == Qt::Horizontal) {
Chris@191:         subclip = QRect(bw, bw+1, width() - bw*2, height() - bw*2 - 2);
Chris@191:     } else {
Chris@191:         subclip = QRect(bw+1, bw, width() - bw*2 - 2, height() - bw*2);
Chris@191:     }
Chris@191: 
Chris@382:     QPainter paint(&m_cache);
Chris@382:     paint.setClipRect(rect());
Chris@191:     paint.fillRect(subclip, palette().background().color());
Chris@190: 
Chris@190:     paint.setRenderHint(QPainter::Antialiasing, true);
Chris@133: 
Chris@908:     double w  = width();
Chris@908:     double w0 = 0.5;
Chris@908:     double w1 = w - 0.5;
Chris@190: 
Chris@908:     double h  = height();
Chris@908:     double h0 = 0.5;
Chris@908:     double h1 = h - 0.5;
Chris@190: 
Chris@190:     for (int i = bw-1; i >= 0; --i) {
Chris@190: 
Chris@133:         int grey = (i + 1) * (256 / (bw + 1));
Chris@133:         QColor fc = QColor(grey, grey, grey);
Chris@133:         paint.setPen(fc);
Chris@190: 
Chris@190:         QPainterPath path;
Chris@190: 
Chris@190:         if (m_orientation == Qt::Horizontal) {
Chris@190:             path.moveTo(w0 + i, h0 + i + 2);
Chris@190:             path.quadTo(w/2, i * 1.25, w1 - i, h0 + i + 2);
Chris@190:             path.lineTo(w1 - i, h1 - i - 2);
Chris@190:             path.quadTo(w/2, h - i * 1.25, w0 + i, h1 - i - 2);
Chris@190:             path.closeSubpath();
Chris@190:         } else {
Chris@190:             path.moveTo(w0 + i + 2, h0 + i);
Chris@190:             path.quadTo(i * 1.25, h/2, w0 + i + 2, h1 - i);
Chris@190:             path.lineTo(w1 - i - 2, h1 - i);
Chris@190:             path.quadTo(w - i * 1.25, h/2, w1 - i - 2, h0 + i);
Chris@190:             path.closeSubpath();
Chris@190:         }
Chris@190: 
Chris@190:         paint.drawPath(path);
Chris@133:     }
Chris@133: 
Chris@191:     paint.setClipRect(subclip);
Chris@133: 
Chris@908:     double radians = m_rotation * 1.5f * M_PI;
Chris@132: 
Chris@682: //    cerr << "value = " << m_value << ", min = " << m_min << ", max = " << m_max << ", rotation = " << rotation << endl;
Chris@132: 
Chris@190:     w = (m_orientation == Qt::Horizontal ? width() : height()) - bw*2;
Chris@132: 
Chris@132:     // total number of notches on the entire wheel
Chris@132:     int notches = 25;
Chris@132:     
Chris@132:     // radius of the wheel including invisible part
Chris@249:     int radius = int(w / 2 + 2);
Chris@132: 
Chris@132:     for (int i = 0; i < notches; ++i) {
Chris@132: 
Chris@908:         double a0 = (2.0 * M_PI * i) / notches + radians;
Chris@908:         double a1 = a0 + M_PI / (notches * 2);
Chris@908:         double a2 = (2.0 * M_PI * (i + 1)) / notches + radians;
Chris@132: 
Chris@908:         double depth = cos((a0 + a2) / 2);
Chris@132:         if (depth < 0) continue;
Chris@132: 
Chris@908:         double x0 = radius * sin(a0) + w/2;
Chris@908:         double x1 = radius * sin(a1) + w/2;
Chris@908:         double x2 = radius * sin(a2) + w/2;
Chris@132:         if (x2 < 0 || x0 > w) continue;
Chris@132: 
Chris@132:         if (x0 < 0) x0 = 0;
Chris@132:         if (x2 > w) x2 = w;
Chris@132: 
Chris@133:         x0 += bw;
Chris@133:         x1 += bw;
Chris@133:         x2 += bw;
Chris@133: 
Chris@908:         int grey = int(lrint(120 * depth));
Chris@541: 
Chris@132:         QColor fc = QColor(grey, grey, grey);
Chris@541:         QColor oc = palette().highlight().color();
Chris@132: 
Chris@541:         paint.setPen(fc);
Chris@132: 
Chris@132:         if (m_showScale) {
Chris@132: 
Chris@132:             paint.setBrush(oc);
Chris@132: 
Chris@908:             double prop;
Chris@132:             if (i >= notches / 4) {
Chris@908:                 prop = double(notches - (((i - double(notches) / 4.f) * 4.f) / 3.f))
Chris@132:                     / notches;
Chris@132:             } else {
Chris@132:                 prop = 0.f;
Chris@132:             }
Chris@132:             
Chris@132:             if (m_orientation == Qt::Horizontal) {
Chris@133:                 paint.drawRect(QRectF(x1, height() - (height() - bw*2) * prop - bw,
Chris@132:                                       x2 - x1, height() * prop));
Chris@132:             } else {
Chris@133:                 paint.drawRect(QRectF(bw, x1, (width() - bw*2) * prop, x2 - x1));
Chris@132:             }
Chris@132:         }
Chris@132: 
Chris@541:         paint.setPen(fc);
Chris@132:         paint.setBrush(palette().background().color());
Chris@132: 
Chris@132:         if (m_orientation == Qt::Horizontal) {
Chris@133:             paint.drawRect(QRectF(x0, bw, x1 - x0, height() - bw*2));
Chris@132:         } else {
Chris@133:             paint.drawRect(QRectF(bw, x0, width() - bw*2, x1 - x0));
Chris@132:         }
Chris@132:     }
Chris@382: 
Chris@382:     QPainter paint2(this);
Chris@382:     paint2.drawImage(0, 0, m_cache);
Chris@132: }
Chris@132: 
Chris@132: QSize
Chris@132: Thumbwheel::sizeHint() const
Chris@132: {
Chris@132:     if (m_orientation == Qt::Horizontal) {
Chris@132:         return QSize(80, 12);
Chris@132:     } else {
Chris@132:         return QSize(12, 80);
Chris@132:     }
Chris@132: }
Chris@132: