/*
Copyright © 2017 Hasan Yavuz Özderya
This file is part of serialplot.
serialplot 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 3 of the License, or
(at your option) any later version.
serialplot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with serialplot. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include "scalepicker.h"
// minimum size for pick (in pixels)
#define MIN_PICK_SIZE (2)
#define SNAP_DISTANCE (5)
class PlotOverlay : public QwtWidgetOverlay
{
public:
PlotOverlay(QWidget* widget, ScalePicker* picker);
protected:
virtual void drawOverlay(QPainter*) const;
private:
ScalePicker* _picker;
};
PlotOverlay::PlotOverlay(QWidget* widget, ScalePicker* picker) :
QwtWidgetOverlay(widget)
{
_picker = picker;
}
void PlotOverlay::drawOverlay(QPainter* painter) const
{
_picker->drawPlotOverlay(painter);
}
class ScaleOverlay : public QwtWidgetOverlay
{
public:
ScaleOverlay(QWidget* widget, ScalePicker* picker);
protected:
virtual void drawOverlay(QPainter*) const;
private:
ScalePicker* _picker;
};
ScaleOverlay::ScaleOverlay(QWidget* widget, ScalePicker* picker) :
QwtWidgetOverlay(widget)
{
_picker = picker;
}
void ScaleOverlay::drawOverlay(QPainter* painter) const
{
_picker->drawScaleOverlay(painter);
}
ScalePicker::ScalePicker(QwtScaleWidget* scaleWidget, QWidget* canvas) :
QObject(scaleWidget)
{
_scaleWidget = scaleWidget;
_canvas = canvas;
scaleWidget->installEventFilter(this);
scaleWidget->setMouseTracking(true);
pickerOverlay = new PlotOverlay(canvas, this);
scaleOverlay = new ScaleOverlay(scaleWidget, this);
started = false;
pressed = false;
}
bool ScalePicker::eventFilter(QObject* object, QEvent* event)
{
if (event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::MouseButtonRelease ||
event->type() == QEvent::MouseMove)
{
updateSnapPoints();
QMouseEvent* mouseEvent = (QMouseEvent*) event;
int posPx = this->positionPx(mouseEvent);
// do snapping unless Shift is pressed
if (! (mouseEvent->modifiers() & Qt::ShiftModifier))
{
for (auto sp : snapPoints)
{
if (std::abs(posPx-sp) <= SNAP_DISTANCE)
{
posPx = sp;
break;
}
}
}
double pos = this->position(posPx);
currentPosPx = posPx;
if (event->type() == QEvent::MouseButtonPress &&
mouseEvent->button() == Qt::LeftButton)
{
pressed = true; // not yet started
firstPos = pos;
firstPosPx = posPx;
}
else if (event->type() == QEvent::MouseMove)
{
// make sure pick size is big enough, so that just
// clicking won't trigger pick
if (!started && pressed && (fabs(posPx-firstPosPx) > MIN_PICK_SIZE))
{
started = true;
emit pickStarted(pos);
}
else if (started)
{
emit picking(firstPos, pos);
}
pickerOverlay->updateOverlay();
scaleOverlay->updateOverlay();
}
else // event->type() == QEvent::MouseButtonRelease
{
pressed = false;
if (started)
{
// finalize
started = false;
emit picked(firstPos, pos);
}
}
return true;
}
else if (event->type() == QEvent::Leave)
{
scaleOverlay->updateOverlay();
pickerOverlay->updateOverlay();
return true;
}
else
{
return QObject::eventFilter(object, event);
}
}
const int TEXT_MARGIN = 4;
void ScalePicker::drawPlotOverlay(QPainter* painter)
{
const double FILL_ALPHA = 0.2;
painter->save();
painter->setPen(_pen);
if (started)
{
QColor color = _pen.color();
color.setAlphaF(FILL_ALPHA);
painter->setBrush(color);
QRect rect;
QwtText text = trackerText();
auto tSize = text.textSize(painter->font());
if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale ||
_scaleWidget->alignment() == QwtScaleDraw::TopScale)
{
int canvasHeight = painter->device()->height();
int pickWidth = currentPosPx-firstPosPx;
rect = QRect(posCanvasPx(firstPosPx), 0, pickWidth, canvasHeight);
}
else // vertical
{
int canvasWidth = painter->device()->width();
int pickHeight = currentPosPx-firstPosPx;
rect = QRect(0, posCanvasPx(firstPosPx), canvasWidth, pickHeight);
}
painter->drawRect(rect);
text.draw(painter, pickTrackerTextRect(painter, rect, tSize));
}
else if (_scaleWidget->underMouse())
{
// draw tracker text centered on cursor
QwtText text = trackerText();
auto tsize = text.textSize(painter->font());
text.draw(painter, trackerTextRect(painter, currentPosPx, tsize));
}
painter->restore();
}
QwtText ScalePicker::trackerText() const
{
double pos;
// use stored value if snapped to restore precision
if (snapPointMap.contains(currentPosPx))
{
pos = snapPointMap[currentPosPx];
}
else
{
pos = position(currentPosPx);
}
return QwtText(QString("%1").arg(pos));
}
QRectF ScalePicker::trackerTextRect(QPainter* painter, int posPx, QSizeF textSize) const
{
int canvasPosPx = posCanvasPx(posPx);
QPointF topLeft;
if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale ||
_scaleWidget->alignment() == QwtScaleDraw::TopScale)
{
int left = canvasPosPx - textSize.width() / 2;
int canvasWidth = painter->device()->width();
left = std::max(TEXT_MARGIN, left);
left = std::min(double(left), canvasWidth - textSize.width() - TEXT_MARGIN);
int top = 0;
if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale)
{
top = painter->device()->height() - textSize.height();
}
topLeft = QPointF(left, top);
}
else // left/right scales
{
int top = canvasPosPx-textSize.height() / 2;
int canvasHeight = painter->device()->height();
top = std::max(0, top);
top = std::min(double(top), canvasHeight - textSize.height());
int left = TEXT_MARGIN;
if (_scaleWidget->alignment() == QwtScaleDraw::RightScale)
{
left = painter->device()->width() - textSize.width();
}
topLeft = QPointF(left, top);
}
return QRectF(topLeft, textSize);
}
QRectF ScalePicker::pickTrackerTextRect(QPainter* painter, QRect pickRect, QSizeF textSize) const
{
qreal left = 0;
int pickLength = currentPosPx - firstPosPx;
QPointF topLeft;
if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale ||
_scaleWidget->alignment() == QwtScaleDraw::TopScale)
{
int canvasWidth = painter->device()->width();
if (pickLength > 0)
{
left = pickRect.right() + TEXT_MARGIN;
}
else
{
left = pickRect.right() - (textSize.width() + TEXT_MARGIN);
}
// make sure text is not off the canvas
if (left < TEXT_MARGIN)
{
left = std::max(0, pickRect.right()) + TEXT_MARGIN;
}
else if (left + textSize.width() + TEXT_MARGIN > canvasWidth)
{
left = std::min(pickRect.right(), canvasWidth) - (textSize.width() + TEXT_MARGIN);
}
if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale)
{
int canvasHeight = painter->device()->height();
topLeft = QPointF(left, canvasHeight - textSize.height());
}
else // top scale
{
topLeft = QPointF(left, 0);
}
}
else // left/right scale
{
int canvasHeight = painter->device()->height();
int top = 0;
if (pickLength > 0)
{
top = pickRect.bottom();
}
else
{
top = pickRect.bottom() - textSize.height();
}
// make sure text is not off the canvas
if (top < 0)
{
top = std::max(0, pickRect.bottom());
}
else if (top + textSize.height() > canvasHeight)
{
top = std::min(canvasHeight, pickRect.bottom()) - textSize.height();
}
if (_scaleWidget->alignment() == QwtScaleDraw::LeftScale)
{
topLeft = QPointF(TEXT_MARGIN, top);
}
else // right scale
{
int canvasWidth = painter->device()->width();
topLeft = QPointF(canvasWidth - textSize.width() - TEXT_MARGIN, top);
}
}
return QRectF(topLeft, textSize);
}
void ScalePicker::drawScaleOverlay(QPainter* painter)
{
painter->save();
// rotate & adjust coordinate system for vertical drawing
if (_scaleWidget->alignment() == QwtScaleDraw::LeftScale ||
_scaleWidget->alignment() == QwtScaleDraw::RightScale) // vertical
{
int width = painter->device()->width();
painter->rotate(90);
painter->translate(0, -width);
}
// draw the indicators
if (started) drawTriangle(painter, firstPosPx);
if (started || _scaleWidget->underMouse())
{
drawTriangle(painter, currentPosPx);
}
painter->restore();
}
void ScalePicker::drawTriangle(QPainter* painter, int position)
{
const double tan60 = 1.732;
const double trsize = 10;
const int TRIANGLE_NUM_POINTS = 3;
const int MARGIN = 2;
const QPointF points[TRIANGLE_NUM_POINTS] =
{
{0, 0},
{-trsize/tan60 , trsize},
{trsize/tan60 , trsize}
};
painter->save();
painter->setPen(Qt::NoPen);
painter->setBrush(_scaleWidget->palette().windowText());
painter->setRenderHint(QPainter::Antialiasing);
painter->translate(position, MARGIN);
painter->drawPolygon(points, TRIANGLE_NUM_POINTS);
painter->restore();
}
void ScalePicker::setPen(QPen pen)
{
_pen = pen;
}
// convert the position of the click to the plot coordinates
double ScalePicker::position(double posPx) const
{
return _scaleWidget->scaleDraw()->scaleMap().invTransform(posPx);
}
int ScalePicker::positionPx(QMouseEvent* mouseEvent)
{
double pos;
if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale ||
_scaleWidget->alignment() == QwtScaleDraw::TopScale)
{
pos = mouseEvent->pos().x();
}
else // left or right scale
{
pos = mouseEvent->pos().y();
}
return pos;
}
/*
* Scale widget and canvas widget is not always aligned. Especially
* when zooming scaleWidget moves around. This causes irregularities
* when drawing the tracker lines. This function maps scale widgets
* pixel coordinate to canvas' coordinate.
*/
double ScalePicker::posCanvasPx(double pos) const
{
// assumption: scale.width < canvas.width && scale.x > canvas.x
if (_scaleWidget->alignment() == QwtScaleDraw::BottomScale ||
_scaleWidget->alignment() == QwtScaleDraw::TopScale)
{
return pos + (_scaleWidget->x() - _canvas->x());
}
else // left or right scale
{
return pos + (_scaleWidget->y() - _canvas->y());
}
return pos;
}
void ScalePicker::updateSnapPoints()
{
auto allTicks = _scaleWidget->scaleDraw()->scaleDiv().ticks(QwtScaleDiv::MajorTick) +
_scaleWidget->scaleDraw()->scaleDiv().ticks(QwtScaleDiv::MediumTick) +
_scaleWidget->scaleDraw()->scaleDiv().ticks(QwtScaleDiv::MinorTick);
snapPoints.clear();
snapPointMap.clear();
for(auto t : allTicks)
{
// `round` is used because `allTicks` is double but `snapPoints` is int
int p = round(_scaleWidget->scaleDraw()->scaleMap().transform(t));
snapPoints << p;
snapPointMap[p] = t;
}
}