/* Copyright © 2020 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 "zoomer.h" #include #include #include #include #include static const int VALUE_POINT_DIAM = 4; static const int VALUE_TEXT_MARGIN = VALUE_POINT_DIAM + 2; Zoomer::Zoomer(QWidget* widget, bool doReplot) : ScrollZoomer(widget) { is_panning = false; setTrackerMode(AlwaysOn); // set corner widget between the scrollbars with default background color auto cornerWidget = new QWidget(); auto bgColor = cornerWidget->palette().color(QPalette::Window).name(); auto styleSheet = QString("background-color:%1;").arg(bgColor); cornerWidget->setStyleSheet(styleSheet); ScrollZoomer::setCornerWidget(cornerWidget); } void Zoomer::zoom(int up) { ScrollZoomer::zoom(up); if(zoomRectIndex() == 0) { emit unzoomed(); } } void Zoomer::zoom( const QRectF & rect) { // set the zoom base when user zooms in to first level if (zoomRectIndex() == 0) { this->setZoomBase(false); } ScrollZoomer::zoom(rect); } void Zoomer::setDispChannels(QVector channels) { dispChannels = channels; } QwtText Zoomer::trackerTextF(const QPointF& pos) const { QwtText b = ScrollZoomer::trackerTextF(pos); const QPolygon pa = selection(); if (!isActive() || pa.count() < 2) { return b; } const QRectF rect = invTransform(QRect(pa.first(), pa.last()).normalized()); QString sizeText = QString(" [%1, %2]").\ arg(rect.width(), 0, 'g', 4).\ arg(rect.height(), 0, 'g', 4); b.setText(b.text() + sizeText); return b; } void Zoomer::drawRubberBand(QPainter* painter) const { const double FILL_ALPHA = 0.2; QColor color = painter->pen().color(); color.setAlphaF(FILL_ALPHA); painter->setBrush(color); ScrollZoomer::drawRubberBand(painter); } QRegion Zoomer::rubberBandMask() const { const QPolygon pa = selection(); if (pa.count() < 2) { return QRegion(); } const QRect r = QRect(pa.first(), pa.last()).normalized().adjusted(0, 0, 1, 1); return QRegion(r); } void Zoomer::drawTracker(QPainter* painter) const { if (isActive()) { QwtPlotZoomer::drawTracker(painter); } else if (dispChannels.length()) { drawValues(painter); } } QList Zoomer::visChannels() const { QList result; for (unsigned ci = 0; ci < (unsigned) dispChannels.length(); ci++) { if (dispChannels[ci]->visible()) result.append(dispChannels[ci]); } return result; } const double ValueLabelHeight = 12; // TODO: calculate struct ChannelValue { const StreamChannel* ch; double value; double y; double top() const {return y;}; double bottom() const {return y + ValueLabelHeight;}; }; static void layoutValues(QList& values) { typedef ChannelValue LayItem; typedef QList LayItemList; struct LayGroup { struct VRange {double top, bottom;}; LayItemList items; LayGroup(LayItem* initialItem) {items.append(initialItem);} unsigned numItems() const {return items.size();} double top() const {return items.first()->top();} double bottom() const {return items.last()->bottom();} VRange vRange() const {return {top(), bottom()};} double overlap(const LayGroup* otrGroup) const { auto myr = vRange(); auto otr = otrGroup->vRange(); double a = myr.bottom - otr.top; double b = otr.bottom - myr.top; if (a > 0 and b > 0) { return std::min(a, b); } return 0; } void moveBy(double y) {for (auto it : items) it->y += y;} void join(LayGroup* other) { // assumes other group is below this one and they overlap double ovr_h = overlap(other); // groups are moved less if they have more items and vice versa double ratio = double(numItems()) / double(numItems() + other->numItems()); double self_off = ovr_h * (1. - ratio); // make sure we don't go out of screen (above) after the shift double final_top = top() - self_off; if (final_top < 0) self_off += final_top; // move groups moveBy(-self_off); // up other->moveBy(ovr_h - self_off); // down // finalize the merge by gettin items from other do { items.append(other->items.takeFirst()); } while (!other->items.isEmpty()); } }; // create initial groups (1 group per item) QList groups; for (auto& val : values) groups.append(new LayGroup(&val)); // sort groups according to their items position struct { bool operator()(LayGroup* a, LayGroup* b) const { return a->top() < b->top(); } } compTops; std::sort(groups.begin(), groups.end(), compTops); // do spacing bool somethingOverlaps = true; while (somethingOverlaps and groups.size() > 1) { somethingOverlaps = false; for (int i = 0; i < groups.size() - 1; i++) { auto a = groups[i]; auto b = groups[i + 1]; // make sure nothing is over the top if (a->top() < 0) a->moveBy(-a->top()); // join if groups overlap if (a->overlap(b)) { somethingOverlaps = true; a->join(b); delete groups.takeAt(i + 1); break; } } } // cleanup do { delete groups.takeFirst(); } while (!groups.isEmpty()); }; void Zoomer::drawValues(QPainter* painter) const { auto tpos = trackerPosition(); if (tpos.x() < 0) return; // cursor not on window // find Y values for current cursor X position double x = invTransform(tpos).x(); auto channels = visChannels(); QList values; for (auto ch : channels) { double value = ch->findValue(x); if (!std::isnan(value)) { auto point = transform(QPointF(x, value)); values.append({ch, value, double(point.y())}); } } // TODO should keep? if (values.isEmpty()) { return; } layoutValues(values); painter->save(); // draw vertical line auto linePen = rubberBandPen(); linePen.setStyle(Qt::DotLine); painter->setPen(linePen); const QRect pRect = pickArea().boundingRect().toRect(); int px = tpos.x(); painter->drawLine(px, pRect.top(), px, pRect.bottom()); // draw sample values for (auto value : values) { double val = value.value; auto ch = value.ch; auto point = transform(QPointF(x, val)); painter->setBrush(ch->color()); painter->setPen(Qt::NoPen); painter->drawEllipse(point, VALUE_POINT_DIAM, VALUE_POINT_DIAM); painter->setPen(rubberBandPen()); // We give a very small (1x1) rectangle but disable clipping painter->drawText(QRectF(point.x() + VALUE_TEXT_MARGIN, value.y, 1, 1), Qt::AlignVCenter | Qt::TextDontClip, QString("%1").arg(val)); } painter->restore(); } QRect Zoomer::trackerRect(const QFont& font) const { if (isActive()) { return QwtPlotZoomer::trackerRect(font); } else { return valueTrackerRect(font); } } QRect Zoomer::valueTrackerRect(const QFont& font) const { // TODO: consider using actual tracker values for width calculation const int textWidth = qCeil(QwtText("-8.8888888").textSize(font).width()); const int width = textWidth + VALUE_POINT_DIAM + VALUE_TEXT_MARGIN; const int x = trackerPosition().x() - VALUE_POINT_DIAM; const auto pickRect = pickArea().boundingRect(); return QRect(x, pickRect.y(), width, pickRect.height()); } void Zoomer::widgetMousePressEvent(QMouseEvent* mouseEvent) { if (mouseEvent->modifiers() & Qt::ControlModifier) { is_panning = true; parentWidget()->setCursor(Qt::ClosedHandCursor); pan_point = invTransform(mouseEvent->pos()); } else { ScrollZoomer::widgetMousePressEvent(mouseEvent); } } void Zoomer::widgetMouseMoveEvent(QMouseEvent* mouseEvent) { if (is_panning) { auto cur_point = invTransform(mouseEvent->pos()); auto delta = cur_point - pan_point; moveBy(-delta.x(), -delta.y()); pan_point = invTransform(mouseEvent->pos()); } else { ScrollZoomer::widgetMouseMoveEvent(mouseEvent); } } void Zoomer::widgetMouseReleaseEvent(QMouseEvent* mouseEvent) { if (is_panning) { is_panning = false; parentWidget()->setCursor(Qt::CrossCursor); } else { ScrollZoomer::widgetMouseReleaseEvent(mouseEvent); } }