// Ruler class implementation // // A QWidget displaying a ruler and allowing user zooming and shifting // // QATSH Copyright 2009 Jean-Philippe MEURET #include #include #include #include #include #include #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& qlSecondDivisors = _pSpec->secondaryDivisors(); const QList& 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; } } }