soundeditor/qatsh/Ruler.cpp

482 lines
15 KiB
C++
Raw Normal View History

// Ruler class implementation
//
// A QWidget displaying a ruler and allowing user zooming and shifting
//
// QATSH Copyright 2009 Jean-Philippe MEURET <jpmeuret@free.fr>
#include <iostream>
#include <QPainter>
#include <QPalette>
#include <QFontMetrics>
#include <QResizeEvent>
#include <QWheelEvent>
#include "ATSMath.h"
#include "Ruler.h"
#include "RulerSpec.h"
Ruler::Ruler(QWidget *parent)
: QWidget(parent), _bShifting(false), _dZoomFactor(1.0), _dValueOffset(0.0)
{
//std::cout << "Ruler::Ruler : viewRect = "
// << width() << "x" << height() << std::endl;
}
void Ruler::onSpecChanged(const RulerSpec* pSpec)
{
_pSpec = pSpec;
// Check and fix value offset / zoom factor in case they are no longer
// compatible with new ruler spec.
const double dScaleMinValue = _pSpec->minValue();
const double dScaleMaxValue = _pSpec->maxValue();
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
const double dViewMaxValue =
dScaleMinValue + _dValueOffset + dScaleLength / _dZoomFactor;
if (dViewMaxValue > dScaleMaxValue)
_dValueOffset = dScaleMaxValue - dScaleLength / _dZoomFactor;
if (_dValueOffset < 0.0)
{
_dZoomFactor = 1.0;
_dValueOffset = 0.0;
}
// Force a resize, in order for each ruler-driven view
// to be notified of new offset and zoom factor.
resize(size());
}
void Ruler::zoom(double dRelativeAnchor, double dRelativeFactor)
{
const QPoint qpAnchorPoint((int)(width() * dRelativeAnchor),
(int)(height() * dRelativeAnchor));
zoom(qpAnchorPoint, dRelativeFactor);
}
void Ruler::zoomAll()
{
// Back to unzoomed aspect.
_dZoomFactor = 1.0;
_dValueOffset = 0.0;
// Redraw the ruler.
repaint();
// Post zoom event properties for other views to apply it.
const int nViewLength = _pSpec->orientation() == RulerSpec::eHorizontal ? width() : height();
propagateZoomShift(nViewLength);
}
// Event handlers =============================================================
void Ruler::mousePressEvent(QMouseEvent *pqEvent)
{
if (pqEvent->button() == Qt::LeftButton)
{
_bShifting = true;
_qpLastShiftingPos = pqEvent->pos();
}
}
void Ruler::mouseMoveEvent(QMouseEvent *pqEvent)
{
if ((pqEvent->buttons() & Qt::LeftButton) && _bShifting)
{
shift(pqEvent->pos());
_qpLastShiftingPos = pqEvent->pos();
}
}
void Ruler::mouseReleaseEvent(QMouseEvent *pqEvent)
{
if (pqEvent->button() == Qt::LeftButton && _bShifting)
{
_bShifting = false;
shift(pqEvent->pos());
}
}
void Ruler::wheelEvent(QWheelEvent *pqEvent)
{
//std::cout << "Ruler::zoom : delta=" << pqEvent->delta() << std::endl;
zoom(pqEvent->pos(), pow(2.0, -pqEvent->delta() / 240.0));
}
void Ruler::resizeEvent(QResizeEvent *pqEvent)
{
// std::cout << "Ruler::resizeEvent(" << pqEvent->size().width()
// << "x" << pqEvent->size().height() << ")" << std::endl;
resize(pqEvent->size());
}
// Actually draw the ruler (triggered after a call repaint()).
void Ruler::paintEvent(QPaintEvent *pqEvent)
{
Q_UNUSED(pqEvent);
QPainter qPainter(this);
draw(qPainter);
}
// ===========================================================================
void Ruler::propagateZoomShift(int nViewLength)
{
// Get ruler spec.
const double dScaleMinValue = _pSpec->minValue();
const double dScaleMaxValue = _pSpec->maxValue();
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
//
double dAbsoluteFactor = _dZoomFactor * nViewLength / dScaleLength;
if (bInverted)
dAbsoluteFactor = -dAbsoluteFactor;
//
double dAbsoluteShiftValue = - dAbsoluteFactor;
if (bInverted)
dAbsoluteShiftValue *= _dValueOffset + dScaleLength / _dZoomFactor;
else
dAbsoluteShiftValue *= _dValueOffset;
// Post zoom event properties for other views to apply it.
emit zoomShift(dAbsoluteFactor, dAbsoluteShiftValue);
}
void Ruler::resize(const QSize& qSize)
{
// Get ruler spec.
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
// Get new view length.
const int nViewLength = bHorizontal ? qSize.width() : qSize.height();
// Post zoom event properties for other views to apply it.
propagateZoomShift(nViewLength);
}
void Ruler::zoom(const QPoint& qpAnchorPos, double dRelativeFactor)
{
// Get ruler spec.
const double dScaleMinValue = _pSpec->minValue();
const double dScaleMaxValue = _pSpec->maxValue();
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
// Keep from zooming under 1.0 factor.
if (_dZoomFactor == 1.0 && dRelativeFactor < 1.0)
return;
// Determine new absolute zoom factor (>= 1.0).
const double dOldZoomFactor = _dZoomFactor;
_dZoomFactor *= dRelativeFactor;
if (_dZoomFactor < 1.01)
{
// Prevent zoom factor from being < 1, force it to 1 when positive but close enough,
// and force offset to 0 in such cases (to fix errors due to repeated * and /)
_dZoomFactor = 1.0;
_dValueOffset = 0.0;
dRelativeFactor = _dZoomFactor / dOldZoomFactor;
}
// Determine the new view offset (but keep it inside [0, scaleMax-viewMaxVal+viewMinVal]).
const int nViewLength = bHorizontal ? width() : height();
const double dOldPixelValue = dScaleLength / dOldZoomFactor / nViewLength;
const double dNewPixelValue = dScaleLength / _dZoomFactor / nViewLength;
int nValue = bHorizontal ? qpAnchorPos.x() : qpAnchorPos.y();
if (bInverted)
nValue = nViewLength - 1 - nValue;
_dValueOffset += nValue * (dOldPixelValue - dNewPixelValue);
if (_dValueOffset < 0 || _dValueOffset + dScaleLength / _dZoomFactor > dScaleMaxValue)
_dValueOffset = 0;
// Note: No need to update _dZoomFactor, as already forced to >= 1.0
// Redraw the ruler.
repaint();
// std::cout << "Ruler::wheelEvent(relFact=" << dRelativeFactor
// << ", anchor=" << nValue
// << ") : zoomFactor=" << _dZoomFactor
// << ", offsetValue=" << _dValueOffset << std::endl;
// Post zoom event properties for other views to apply it.
propagateZoomShift(nViewLength);
}
void Ruler::shift(const QPoint& qpNewPos)
{
// Get ruler spec.
const double dScaleMinValue = _pSpec->minValue();
const double dScaleMaxValue = _pSpec->maxValue();
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
// Compute view properties.
const int nViewLength = bHorizontal ? width() : height();
const double dPixelValue = dScaleLength / _dZoomFactor / nViewLength;
// Compute the shifting amount
int nShift;
if (bHorizontal)
nShift = _qpLastShiftingPos.x() - qpNewPos.x();
else
nShift = _qpLastShiftingPos.y() - qpNewPos.y();
if (bInverted)
nShift = - nShift;
// Update current offset from scale origin.
_dValueOffset += nShift * dPixelValue;
if (_dValueOffset < 0)
_dValueOffset = 0;
const double dNewViewMaxValue =
dScaleMinValue + _dValueOffset + dScaleLength / _dZoomFactor;
if (dNewViewMaxValue > dScaleMaxValue)
_dValueOffset = dScaleMaxValue - nViewLength * dPixelValue;
//_dValueOffset = dScaleMaxValue - dScaleLength / _dZoomFactor;
// Redraw the scale.
repaint();
// std::cout << "Ruler::shift : shift=" << nShift
// << ", offsetValue=" << _dValueOffset
// << ", absFactor=" << _dZoomFactor
// << ", pixValue=" << dPixelValue << std::endl;
// Post zoom event properties for other views to apply it.
propagateZoomShift(nViewLength);
}
//===================================================================================
// Draw the scale according to the ruler spec and current zoom factor and translation
static const int nUnitPrefixMinIndex = -3;
static const int nUnitPrefixMaxIndex = +3;
static const int nUnitPrefixes = nUnitPrefixMaxIndex - nUnitPrefixMinIndex + 1;
static const char* pszUnitPrefix[nUnitPrefixes] = { "n", "u", "m", "", "k", "M", "g" };
void Ruler::draw(QPainter& qPainter)
{
qPainter.setPen(Qt::black);
// Get ruler specs.
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
const bool bPositiveSide = _pSpec->marksSide() == RulerSpec::ePositiveSide;
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
typedef enum { eNorth = 0, eWest, eSouth, eEast } EOrientation;
const EOrientation eOrient =
(bHorizontal ? (bPositiveSide ? eNorth : eSouth) : (bPositiveSide ? eEast : eWest));
const double dScaleMinValue = _pSpec->minValue();
const double dScaleMaxValue = _pSpec->maxValue();
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
// Get widget dimensions.
const int nViewLength = bHorizontal ? width() : height();
const int nViewThickness = bHorizontal ? height() : width();
// Draw the main line of the ruler.
switch (eOrient)
{
case eNorth:
qPainter.drawLine(0, 0, nViewLength - 1, 0);
break;
case eSouth:
qPainter.drawLine(0, nViewThickness - 1, nViewLength - 1, nViewThickness - 1);
break;
case eWest:
qPainter.drawLine(0, 0, 0, nViewLength - 1);
break;
case eEast:
qPainter.drawLine(nViewThickness - 1, 0, nViewThickness - 1, nViewLength - 1);
break;
}
// Determine the value interval of the view
double dViewMinValue = dScaleMinValue + _dValueOffset;
double dViewMaxValue = dViewMinValue + dScaleLength / _dZoomFactor;
// Determine the resolution of a pixel in the view
const double dPixelValue = dScaleLength / _dZoomFactor / nViewLength;
//std::cout << "Ruler::draw(orient=" << eOrient << ", dir=" << _pSpec->direction()
// << ") : viewMinValue=" << dViewMinValue
// << ", viewMaxValue=" << dViewMaxValue
// << ", pixelValue=" << dPixelValue << std::endl;
// Determine the distance between main scale marks
// (criteria = at least 5 pixels between 1st secondary scale marks).
const int nMinValueStep = 5;
const double dLevelDivisor = _pSpec->levelDivisor();
double dValueStep = _pSpec->baseStep();
//while (dValueStep >= nMinValueStep * dLevelDivisor * dPixelValue)
while (2 * dValueStep > dScaleLength / _dZoomFactor)
{
dValueStep /= dLevelDivisor;
}
// Determine the lowest value of any main scale mark visible in the view
double dFirstMarkValue =
dScaleMinValue + _pSpec->baseOffset()
+ dValueStep
* ceil((dViewMinValue - dScaleMinValue - _pSpec->baseOffset()) / dValueStep);
//std::cout << "Ruler::draw(main) : markLength=" << nViewThickness
// << ", valueStep=" << dValueStep
// << ", 1stMarkValue=" << dFirstMarkValue << std::endl;
// Draw the main scale marks and associated labels
//const QFont qFont("Tahoma", 7); // Windows very good.
//qPainter.setFont(qFont);
const QFontMetrics qFontMtrx = qPainter.fontMetrics();
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
double dValue = dFirstMarkValue;
int nValue;
while (dValue <= dViewMaxValue)
{
// Mark coordinate (pixel) in the "long" direction of the view.
nValue = (int)round((dValue - dViewMinValue) / dPixelValue);
if (nValue == nViewLength)
nValue--;
if (bInverted)
nValue = nViewLength - 1 - nValue;
switch (eOrient)
{
case eNorth:
case eSouth:
qPainter.drawLine(nValue, 0, nValue, nViewThickness - 1);
break;
case eWest:
case eEast:
qPainter.drawLine(0, nValue, nViewThickness - 1, nValue);
break;
}
// Convert value to a user-friendly label string.
const char* pszLabelSign = dValue > 0.0 ? "" : "-";
double dLabelValue = fabs(dValue);
QString qsLabel("0");
if (dValue != 0.0)
{
const int nUnitPrefixIndex = (int)(floor(log10(dLabelValue))) / 3;
if (nUnitPrefixIndex < nUnitPrefixMinIndex || nUnitPrefixIndex > nUnitPrefixMaxIndex)
qsLabel = QString("%1").arg(dValue, 0, 'g', 3);
else
{
dLabelValue /= pow(10.0, 3*nUnitPrefixIndex);
qsLabel = QString("%1%2%3").arg(pszLabelSign).arg(dLabelValue, 0, 'g', 3)
.arg(pszUnitPrefix[nUnitPrefixIndex - nUnitPrefixMinIndex]);
}
}
// Draw the mark label.
const QSize qsLabelSize = qFontMtrx.size(Qt::TextSingleLine, qsLabel);
const int nLabeWidth = qsLabelSize.width();
const int nLabeHeight = qsLabelSize.height();
nValue += (bHorizontal ? (bInverted ? -3 - nLabeWidth : 3) : -3);
switch (eOrient)
{
case eNorth:
qPainter.drawText(nValue, nViewThickness - 1, qsLabel);
break;
case eSouth:
qPainter.drawText(nValue, nLabeHeight, qsLabel);
break;
case eWest:
qPainter.drawText(nViewThickness - nLabeWidth, nValue, qsLabel);
break;
case eEast:
qPainter.drawText(0, nValue, qsLabel);
break;
}
// Next mark.
dValue += dValueStep;
}
// Draw the secondary scale marks
const QList<double>& qlSecondDivisors = _pSpec->secondaryDivisors();
const QList<double>& qlSecondSizes = _pSpec->secondaryMarksSizes();
for (int nSecondInd = 0; nSecondInd < qlSecondDivisors.size(); nSecondInd++)
{
const int nMarkLength = (int)round(qlSecondSizes[nSecondInd] * nViewThickness);
dValueStep /= qlSecondDivisors[nSecondInd];
// Don't draw secondary scales under a certain inter-mark distance threshold.
if (dValueStep < nMinValueStep * dPixelValue)
break;
dFirstMarkValue =
dScaleMinValue + _pSpec->baseOffset()
+ dValueStep
* ceil((dViewMinValue - dScaleMinValue - _pSpec->baseOffset()) / dValueStep);
//dFirstMarkValue = remainder(dFirstMarkValue - dViewMinValue, dValueStep);
dValue = dFirstMarkValue;
//std::cout << "Ruler::draw(secondary) : markLength=" << nMarkLength
// << ", valueStep=" << dValueStep
// << ", 1stMarkValue=" << dFirstMarkValue << std::endl;
while (dValue <= dViewMaxValue)
{
// mark coordinate (pixel) in the "long" direction of the view.
nValue = (int)round((dValue - dViewMinValue) / dPixelValue);
if (nValue == nViewLength)
nValue--;
if (bInverted)
nValue = nViewLength - 1 - nValue;
switch (eOrient)
{
case eNorth:
qPainter.drawLine(nValue, 0, nValue, nMarkLength - 1);
break;
case eSouth:
qPainter.drawLine(nValue, nViewThickness - nMarkLength,
nValue, nViewThickness - 1);
break;
case eWest:
qPainter.drawLine(0, nValue, nMarkLength - 1, nValue);
break;
case eEast:
qPainter.drawLine(nViewThickness - nMarkLength, nValue,
nViewThickness - 1, nValue);
break;
}
dValue += dValueStep;
}
}
}