/* Copyright (c) 2012-2016 Stanislaw Halik * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. */ #include "compat/util.hpp" #include "spline-widget.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include spline_widget::spline_widget(QWidget *parent) : QWidget(parent), _config(nullptr), snap_x(0), snap_y(0), moving_control_point_idx(-1), _draw_function(true), _preview_only(false) { update_range(); setMouseTracking(true); setFocusPolicy(Qt::ClickFocus); setCursor(Qt::ArrowCursor); } spline_widget::~spline_widget() { if (connection) QObject::disconnect(connection); } void spline_widget::setConfig(spline* spl) { if (spl != _config) { if (connection) { QObject::disconnect(connection); //connection = QMetaObject::Connection(); } if (spl) { mem s = spl->get_settings(); connection = connect(s.get(), &spline::settings::recomputed, this, [this]() { reload_spline(); }, Qt::QueuedConnection); } _config = spl; update_range(); } } void spline_widget::set_preview_only(bool val) { _preview_only = val; } bool spline_widget::is_preview_only() const { return _preview_only; } void spline_widget::drawBackground() { if (!_config) return; _background = QPixmap(width(), height()); QPainter painter(&_background); painter.fillRect(rect(), QColor::fromRgb(204, 204, 204)); QColor bg_color(112, 154, 209); if (!isEnabled() && !_preview_only) bg_color = QColor(176,176,180); painter.fillRect(pixel_bounds, bg_color); QFont font; font.setPointSize(8); painter.setFont(font); QFontMetrics metrics(font); QColor color__(176, 190, 209, 127); if (!isEnabled()) color__ = QColor(70, 90, 100, 96); const QPen pen(color__, 1, Qt::SolidLine, Qt::FlatCap); const int xstep = 10, ystep = 10; const qreal maxx = _config->maxInput(); const qreal maxy = _config->maxOutput(); // horizontal grid for (int i = 0; i <= maxy; i += xstep) { const int y = int(pixel_bounds.height() - i * c.y() + pixel_bounds.y()); drawLine(painter, QPoint(pixel_bounds.x(), y), QPoint(pixel_bounds.x() + pixel_bounds.width(), y), pen); painter.drawText(QRectF(10, y - metrics.height()/2., pixel_bounds.left(), metrics.height()), QString::number(i)); } // vertical grid for (int i = 0; i <= maxx; i += ystep) { const int x = int(std::round(pixel_bounds.x() + i * c.x())); drawLine(painter, QPoint(x, pixel_bounds.y()), QPoint(x, pixel_bounds.y() + pixel_bounds.height()), pen); const QString text = QString::number(i); painter.drawText(QRectF(x - metrics.width(text)/2., pixel_bounds.height() + 10 + metrics.height(), metrics.width(text), metrics.height()), text); } } void spline_widget::drawFunction() { if (!_config) return; _function = _background; QPainter painter(&_function); painter.setRenderHint(QPainter::Antialiasing, true); points_t points = _config->getPoints(); const QColor color = progn ( if (!isEnabled() && !_preview_only) { const int avg = int(float(color.red() + color.green() + color.blue())/3); return QColor(int(float(color.red() + avg) * .5f), int(float(color.green() + avg) * .5f), int(float(color.blue() + avg) * .5f), 96); } else { QColor color(spline_color); color.setAlphaF(std::min(.9, color.alphaF())); return color; } ); const qreal step_ = line_length_pixels / c.x(); const qreal step = std::max(5e-2, step_); const qreal max = _config->maxInput(); painter.setPen(QPen(color, 1.75, Qt::SolidLine, Qt::FlatCap)); QPointF prev = point_to_pixel(QPoint(0, 0)); for (qreal i = 0; i < max; i += step) { const qreal val = qreal(_config->getValue(i)); const QPointF cur = point_to_pixel(QPointF(i, val)); painter.drawLine(prev, cur); prev = cur; } { const qreal val = qreal(_config->getValue(max)); const QPointF last = point_to_pixel(QPointF(max, val)); painter.drawLine(prev, last); } const int alpha = !isEnabled() ? 64 : 120; if (!_preview_only) { for (int i = 0; i < points.size(); i++) { drawPoint(painter, point_to_pixel(points[i]), QColor(200, 200, 210, alpha), isEnabled() ? QColor(50, 100, 120, 200) : QColor(200, 200, 200, 96)); } } } void spline_widget::paintEvent(QPaintEvent *e) { QPainter p(this); if (_background.isNull()) { _draw_function = true; drawBackground(); } if (_draw_function) { _draw_function = false; drawFunction(); } p.drawPixmap(e->rect(), _function); if (_config) { const QPen pen(Qt::white, 1, Qt::SolidLine, Qt::FlatCap); points_t points = _config->getPoints(); if (points.size() && moving_control_point_idx >= 0 && moving_control_point_idx < points.size()) { if (points[0].x() > 1e-2) points.push_front(QPoint(0, 0)); const QPointF prev_ = point_to_pixel(points[0]); QPoint prev(int(std::round(prev_.x())), int(std::round(prev_.y()))); for (int i = 1; i < points.size(); i++) { const QPointF tmp_ = point_to_pixel(points[i]); const QPoint tmp(int(std::round(tmp_.x())), int(std::round(tmp_.y()))); drawLine(p, prev, tmp, pen); prev = tmp; } } // If the Tracker is active, the 'Last Point' it requested is recorded. // Show that point on the graph, with some lines to assist. // This new feature is very handy for tweaking the curves! QPointF last; if (_config->getLastPoint(last) && isEnabled()) { drawPoint(p, point_to_pixel(last), QColor(255, 0, 0, 120)); } } } void spline_widget::drawPoint(QPainter& painter, const QPointF& pos, const QColor& colBG, const QColor& border) { painter.save(); painter.setPen(QPen(border, 1, Qt::SolidLine, Qt::PenCapStyle::FlatCap)); painter.setBrush(colBG); painter.drawEllipse(QRectF(pos.x() - point_size, pos.y() - point_size, point_size*2, point_size*2)); painter.restore(); } void spline_widget::drawLine(QPainter& painter, const QPoint& start, const QPoint& end, const QPen& pen) { painter.save(); painter.setPen(pen); painter.setBrush(Qt::NoBrush); painter.drawLine(start, end); painter.restore(); } void spline_widget::mousePressEvent(QMouseEvent *e) { if (!_config || !isEnabled()) return; if (!is_in_bounds(e->pos())) return; points_t points = _config->getPoints(); if (e->button() == Qt::LeftButton) { bool bTouchingPoint = false; moving_control_point_idx = -1; if (_config) { for (int i = 0; i < points.size(); i++) { if (point_within_pixel(points[i], e->pos())) { bTouchingPoint = true; moving_control_point_idx = int(i); break; } } if (!bTouchingPoint) { bool too_close = false; const auto pos = e->pos(); for (int i = 0; i < points.size(); i++) { const QPointF pt = point_to_pixel(points[i]); const qreal x = std::fabs(pt.x() - pos.x()); if (point_closeness_limit >= x) { too_close = true; break; } } if (!too_close) { _config->addPoint(pixel_coord_to_point(e->pos())); show_tooltip(e->pos()); } } } } if (e->button() == Qt::RightButton) { if (_config) { int found_pt = -1; for (int i = 0; i < points.size(); i++) { if (point_within_pixel(points[i], e->pos())) { found_pt = i; break; } } if (found_pt != -1) { _config->removePoint(found_pt); } moving_control_point_idx = -1; } } _draw_function = true; update(); } void spline_widget::mouseMoveEvent(QMouseEvent *e) { if (!_config || !isEnabled() || !isActiveWindow() || (moving_control_point_idx != -1 && !hasFocus())) { clearFocus(); return; } points_t points = _config->getPoints(); if (moving_control_point_idx >= 0 && moving_control_point_idx < points.size()) { const int idx = moving_control_point_idx; setCursor(Qt::ClosedHandCursor); bool overlap = false; QPoint pix = e->pos(); QPointF new_pt = pixel_coord_to_point(pix); for (int i = 0; i < 2; i++) { bool bad = false; if (idx + 1 < points.size()) { auto other = points[idx+1]; auto other_pix = point_to_pixel(other); bad = pix.x() + point_closeness_limit > other_pix.x(); if (i == 0 && bad) { pix.setX(int(std::round(other_pix.x() - point_closeness_limit))); new_pt = pixel_coord_to_point(pix); } else overlap |= bad; } if (idx != 0) { const QPointF other = points[idx-1]; const QPointF other_pix = point_to_pixel(other); bad = pix.x() - point_closeness_limit < other_pix.x(); if (i == 0 && bad) { pix.setX(int(std::round(other_pix.x() + point_closeness_limit))); new_pt = pixel_coord_to_point(pix); } else overlap |= bad; } if (!bad) break; } if (!overlap) { points[idx] = new_pt; _config->movePoint(int(idx), new_pt); _draw_function = true; show_tooltip(pix, new_pt); update(); } } else { int i; bool is_on_point = is_on_pt(e->pos(), &i); if (is_on_point) { setCursor(Qt::CrossCursor); show_tooltip(e->pos(), points[i]); } else { setCursor(Qt::ArrowCursor); if (is_in_bounds(e->pos())) show_tooltip(e->pos()); else QToolTip::hideText(); } } } void spline_widget::mouseReleaseEvent(QMouseEvent *e) { if (!_config || !isEnabled() || !isActiveWindow() || !hasFocus()) { clearFocus(); return; } if (e->button() == Qt::LeftButton) { //mouseMoveEvent(e); { int i; if (is_on_pt(e->pos(), &i)) { setCursor(Qt::CrossCursor); } else setCursor(Qt::ArrowCursor); } moving_control_point_idx = -1; _draw_function = true; if (is_in_bounds(e->pos())) show_tooltip(e->pos()); else QToolTip::hideText(); update(); } } void spline_widget::reload_spline() { if (_config) { // don't recompute here as the value's just been recomputed update_range(); } } void spline_widget::show_tooltip(const QPoint& pos, const QPointF& value_, const QString& prefix) { const QPointF value = QPoint(0, 0) == value_ ? pixel_coord_to_point(pos) : value_; double x = value.x(), y = value.y(); const int x_ = int(std::round(x)), y_ = int(std::round(y)); using std::fabs; if (fabs(x_ - x) < 1e-3) x = x_; if (fabs(y_ - y) < 1e-3) y = y_; const bool is_fusion = QStringLiteral("fusion") == QApplication::style()->objectName(); const int add_x = (is_fusion ? 25 : 0), add_y = (is_fusion ? 15 : 0); const QPoint pix(int(pos.x()) + add_x, int(pos.y()) + add_y); QToolTip::showText(mapToGlobal(pix), QStringLiteral("value: %1%2x%3").arg(prefix).arg(x).arg(y), this, rect(), 0); } bool spline_widget::is_in_bounds(const QPoint& pos) const { static constexpr int grace = 2; return !(pos.x() + grace < pixel_bounds.left() || pos.x() - grace > pixel_bounds.right() || pos.y() + grace < pixel_bounds.top() || pos.y() - grace > pixel_bounds.bottom()); } void spline_widget::update_range() { if (!_config) return; const int w = width(), h = height(); const int mwl = 40, mhl = 20; const int mwr = 15, mhr = 35; pixel_bounds = QRect(mwl, mhl, (w - mwl - mwr), (h - mhl - mhr)); c = QPointF(pixel_bounds.width() / _config->maxInput(), pixel_bounds.height() / _config->maxOutput()); _draw_function = true; _background = QPixmap(); _function = QPixmap(); update(); } bool spline_widget::point_within_pixel(const QPointF& pt, const QPoint &pixel) { QPointF tmp = pixel - point_to_pixel(pt); return sqrt(QPointF::dotProduct(tmp, tmp)) < point_size; } void spline_widget::focusOutEvent(QFocusEvent* e) { if (moving_control_point_idx != -1) QToolTip::hideText(); moving_control_point_idx = -1; lower(); setCursor(Qt::ArrowCursor); e->accept(); } QPointF spline_widget::pixel_coord_to_point(const QPoint& point) { if (!_config) return QPoint(-1, -1); using std::round; qreal x = (point.x() - pixel_bounds.x()) / c.x(); qreal y = (pixel_bounds.height() - point.y() + pixel_bounds.y()) / c.y(); static constexpr int c = 1000; if (snap_x > 0) { x -= std::fmod(x, snap_x); const volatile int x_ = int(x * c); x = x_ / double(c); } if (snap_y > 0) { y -= std::fmod(y, snap_y); const volatile int y_ = int(y * c); y = y_ / double(c); } if (x < 0) x = 0; if (x > _config->maxInput()) x = _config->maxInput(); if (y < 0) y = 0; if (y > _config->maxOutput()) y = _config->maxOutput(); return QPointF(x, y); } QPointF spline_widget::point_to_pixel(const QPointF& point) { return QPoint(int(std::round(pixel_bounds.x() + point.x() * c.x())), int(std::round(pixel_bounds.y() + pixel_bounds.height() - point.y() * c.y()))); } void spline_widget::resizeEvent(QResizeEvent *) { update_range(); } bool spline_widget::is_on_pt(const QPoint& pos, int* pt) { if (!_config) { if (pt) *pt = -1; return false; } const points_t points = _config->getPoints(); for (int i = 0; i < points.size(); i++) { if (point_within_pixel(points[i], pos)) { if (pt) *pt = i; return true; } } if (pt) *pt = -1; return false; }