File indexing completed on 2025-03-09 04:28:22

0001 /*
0002     SPDX-FileCopyrightText: 2005 C. Boemann <cbo@boemann.dk>
0003     SPDX-FileCopyrightText: 2009 Dmitry Kazakov <dimula73@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 // Local includes.
0009 
0010 #include "kis_curve_widget.h"
0011 #include "kdenlivesettings.h"
0012 // C++ includes.
0013 
0014 #include <cmath>
0015 #include <cstdlib>
0016 
0017 // Qt includes.
0018 
0019 #include <QMouseEvent>
0020 #include <QPaintEvent>
0021 #include <QPainter>
0022 #include <QPen>
0023 #include <QPoint>
0024 #include <QRect>
0025 
0026 #define bounds(x, a, b) (x < a ? a : (x > b ? b : x))
0027 #define MOUSE_AWAY_THRES 15
0028 #define POINT_AREA 1E-4
0029 #define CURVE_AREA 1E-4
0030 
0031 // static bool pointLessThan(const QPointF &a, const QPointF &b);
0032 
0033 KisCurveWidget::KisCurveWidget(QWidget *parent)
0034     : AbstractCurveWidget(parent)
0035     , m_grabOffsetX(0)
0036     , m_grabOffsetY(0)
0037     , m_grabOriginalX(0)
0038     , m_grabOriginalY(0)
0039     , m_draggedAwayPointIndex(0)
0040     , m_guideVisible(false)
0041 
0042 {
0043     setObjectName(QStringLiteral("KisCurveWidget"));
0044 
0045     m_pixmapIsDirty = false;
0046     m_pixmapCache = nullptr;
0047     m_maxPoints = 0;
0048     m_curve = KisCubicCurve();
0049     setMaxPoints(5);
0050     update();
0051 }
0052 
0053 KisCurveWidget::~KisCurveWidget() = default;
0054 
0055 QSize KisCurveWidget::sizeHint() const
0056 {
0057     return {500, 500};
0058 }
0059 
0060 void KisCurveWidget::addPointInTheMiddle()
0061 {
0062     QPointF pt(0.5, m_curve.value(0.5));
0063 
0064     if (!jumpOverExistingPoints(pt, -1)) {
0065         return;
0066     }
0067 
0068     m_currentPointIndex = m_curve.addPoint(pt);
0069 
0070     update();
0071     Q_EMIT modified();
0072 }
0073 
0074 void KisCurveWidget::paintEvent(QPaintEvent *)
0075 {
0076     QPainter p(this);
0077     paintBackground(&p);
0078 
0079     // Draw curve.
0080     int x;
0081     QPolygonF poly;
0082 
0083     p.setPen(QPen(palette().text().color(), 1, Qt::SolidLine));
0084     poly.reserve(m_wWidth);
0085     for (x = 0; x < m_wWidth; ++x) {
0086         double normalizedX = double(x) / m_wWidth;
0087         double curY = m_wHeight - m_curve.value(normalizedX) * m_wHeight;
0088         poly.append(QPointF(x, curY));
0089     }
0090     poly.append(QPointF(x, m_wHeight - m_curve.value(1.0) * m_wHeight));
0091     p.drawPolyline(poly);
0092 
0093     // Drawing curve handles.
0094     for (int i = 0; i < m_curve.points().count(); ++i) {
0095         double curveX = m_curve.points().at(i).x();
0096         double curveY = m_curve.points().at(i).y();
0097 
0098         if (i == m_currentPointIndex) {
0099             p.setPen(QPen(Qt::red, 3, Qt::SolidLine));
0100             p.drawEllipse(QRectF(curveX * m_wWidth - 2, m_wHeight - 2 - curveY * m_wHeight, 4, 4));
0101         } else {
0102             p.setPen(QPen(Qt::red, 1, Qt::SolidLine));
0103             p.drawEllipse(QRectF(curveX * m_wWidth - 3, m_wHeight - 3 - curveY * m_wHeight, 6, 6));
0104         }
0105     }
0106 }
0107 
0108 void KisCurveWidget::mousePressEvent(QMouseEvent *e)
0109 {
0110     int wWidth = width() - 1;
0111     int wHeight = height() - 1;
0112     int offsetX = int(1 / 8. * m_zoomLevel * wWidth);
0113     int offsetY = int(1 / 8. * m_zoomLevel * wHeight);
0114     wWidth -= 2 * offsetX;
0115     wHeight -= 2 * offsetY;
0116     double x = (e->pos().x() - offsetX) / double(wWidth);
0117     double y = 1.0 - (e->pos().y() - offsetY) / double(wHeight);
0118 
0119     int closest_point_index = nearestPointInRange(QPointF(x, y), width(), height());
0120 
0121     if (e->button() == Qt::RightButton && closest_point_index > 0 && closest_point_index < m_curve.points().count() - 1) {
0122         m_currentPointIndex = closest_point_index;
0123         slotDeleteCurrentPoint();
0124         return;
0125     } else if (e->button() != Qt::LeftButton) {
0126         return;
0127     }
0128 
0129     if (closest_point_index < 0) {
0130         if (m_maxPoints > 0 && m_curve.points().count() >= m_maxPoints) {
0131             return;
0132         }
0133         QPointF newPoint(x, y);
0134         if (!jumpOverExistingPoints(newPoint, -1)) {
0135             return;
0136         }
0137         m_currentPointIndex = m_curve.addPoint(newPoint);
0138     } else {
0139         m_currentPointIndex = closest_point_index;
0140     }
0141 
0142     m_grabOriginalX = m_curve.points().at(m_currentPointIndex).x();
0143     m_grabOriginalY = m_curve.points().at(m_currentPointIndex).y();
0144     m_grabOffsetX = m_curve.points().at(m_currentPointIndex).x() - x;
0145     m_grabOffsetY = m_curve.points().at(m_currentPointIndex).y() - y;
0146     QPointF point(x + m_grabOffsetX, y + m_grabOffsetY);
0147     m_curve.setPoint(m_currentPointIndex, point);
0148 
0149     m_draggedAwayPointIndex = -1;
0150     m_state = State_t::DRAG;
0151 
0152     update();
0153     Q_EMIT currentPoint(point, isCurrentPointExtremal());
0154 }
0155 
0156 void KisCurveWidget::mouseMoveEvent(QMouseEvent *e)
0157 {
0158     int wWidth = width() - 1;
0159     int wHeight = height() - 1;
0160     int offsetX = int(1 / 8. * m_zoomLevel * wWidth);
0161     int offsetY = int(1 / 8. * m_zoomLevel * wHeight);
0162     wWidth -= 2 * offsetX;
0163     wHeight -= 2 * offsetY;
0164     double x = (e->pos().x() - offsetX) / double(wWidth);
0165     double y = 1.0 - (e->pos().y() - offsetY) / double(wHeight);
0166 
0167     if (m_state == State_t::NORMAL) { // If no point is selected set the cursor shape if on top
0168         int nearestPointIndex = nearestPointInRange(QPointF(x, y), width(), height());
0169 
0170         if (nearestPointIndex < 0) {
0171             setCursor(Qt::ArrowCursor);
0172         } else {
0173             setCursor(Qt::CrossCursor);
0174         }
0175     } else { // Else, drag the selected point
0176         bool crossedHoriz = e->pos().x() - width() > MOUSE_AWAY_THRES || e->pos().x() < -MOUSE_AWAY_THRES;
0177         bool crossedVert = e->pos().y() - height() > MOUSE_AWAY_THRES || e->pos().y() < -MOUSE_AWAY_THRES;
0178 
0179         bool removePoint = (crossedHoriz || crossedVert);
0180 
0181         if (!removePoint && m_draggedAwayPointIndex >= 0) {
0182             // point is no longer dragged away so reinsert it
0183             QPointF newPoint(m_draggedAwayPoint);
0184             m_currentPointIndex = m_curve.addPoint(newPoint);
0185             m_draggedAwayPointIndex = -1;
0186         }
0187 
0188         if (removePoint && (m_draggedAwayPointIndex >= 0)) {
0189             return;
0190         }
0191 
0192         setCursor(Qt::CrossCursor);
0193 
0194         x += m_grabOffsetX;
0195         y += m_grabOffsetY;
0196 
0197         double leftX;
0198         double rightX;
0199         if (m_currentPointIndex == 0) {
0200             leftX = 0.0;
0201             rightX = 0.0;
0202             /*if (m_curve.points().count() > 1)
0203                 rightX = m_curve.points().at(m_currentPointIndex + 1).x() - POINT_AREA;
0204             else
0205                 rightX = 1.0;*/
0206         } else if (m_currentPointIndex == m_curve.points().count() - 1) {
0207             leftX = m_curve.points().at(m_currentPointIndex - 1).x() + POINT_AREA;
0208             rightX = 1.0;
0209         } else {
0210             Q_ASSERT(m_currentPointIndex > 0 && m_currentPointIndex < m_curve.points().count() - 1);
0211 
0212             // the 1E-4 addition so we can grab the dot later.
0213             leftX = m_curve.points().at(m_currentPointIndex - 1).x() + POINT_AREA;
0214             rightX = m_curve.points().at(m_currentPointIndex + 1).x() - POINT_AREA;
0215         }
0216 
0217         x = bounds(x, leftX, rightX);
0218         y = bounds(y, 0., 1.);
0219         QPointF point(x, y);
0220 
0221         m_curve.setPoint(m_currentPointIndex, point);
0222 
0223         if (removePoint && m_curve.points().count() > 2) {
0224             m_draggedAwayPoint = m_curve.points().at(m_currentPointIndex);
0225             m_draggedAwayPointIndex = m_currentPointIndex;
0226             m_curve.removePoint(m_currentPointIndex);
0227             m_currentPointIndex = bounds(m_currentPointIndex, 0, m_curve.points().count() - 1);
0228         }
0229 
0230         update();
0231         Q_EMIT currentPoint(point, isCurrentPointExtremal());
0232         if (KdenliveSettings::dragvalue_directupdate()) {
0233             Q_EMIT modified();
0234         }
0235     }
0236 }
0237 
0238 bool KisCurveWidget::jumpOverExistingPoints(QPointF &pt, int skipIndex)
0239 {
0240     for (QPointF &it : m_curve.points()) {
0241         if (m_curve.points().indexOf(it) == skipIndex) {
0242             continue;
0243         }
0244         if (fabs(it.x() - pt.x()) < POINT_AREA) pt.rx() = pt.x() >= it.x() ? it.x() + POINT_AREA : it.x() - POINT_AREA;
0245     }
0246     return (pt.x() >= 0 && pt.x() <= 1.);
0247 }
0248 
0249 int KisCurveWidget::nearestPointInRange(QPointF pt, int wWidth, int wHeight) const
0250 {
0251     double nearestDistanceSquared = 1000;
0252     int nearestIndex = -1;
0253     int i = 0;
0254 
0255     for (QPointF &point : m_curve.points()) {
0256         double distanceSquared = (pt.x() - point.x()) * (pt.x() - point.x()) + (pt.y() - point.y()) * (pt.y() - point.y());
0257 
0258         if (distanceSquared < nearestDistanceSquared) {
0259             nearestIndex = i;
0260             nearestDistanceSquared = distanceSquared;
0261         }
0262         ++i;
0263     }
0264 
0265     if (nearestIndex >= 0) {
0266         double dx = (pt.x() - m_curve.points().at(nearestIndex).x()) * wWidth;
0267         double dy = (pt.y() - m_curve.points().at(nearestIndex).y()) * wHeight;
0268         if (dx * dx + dy * dy <= m_grabRadius * m_grabRadius) {
0269             return nearestIndex;
0270         }
0271     }
0272 
0273     return -1;
0274 }
0275 
0276 // void KisCurveWidget::syncIOControls()
0277 // {
0278 //     if (!m_intIn || !m_intOut) {
0279 //         return;
0280 //     }
0281 
0282 //     bool somethingSelected = (m_currentPointIndex >= 0);
0283 
0284 //     m_intIn->setEnabled(somethingSelected);
0285 //     m_intOut->setEnabled(somethingSelected);
0286 
0287 //     if (m_currentPointIndex >= 0) {
0288 //         m_intIn->blockSignals(true);
0289 //         m_intOut->blockSignals(true);
0290 
0291 //         m_intIn->setValue(sp2io(m_curve.points().at(m_currentPointIndex).x()));
0292 //         m_intOut->setValue(sp2io(m_curve.points().at(m_currentPointIndex).y()));
0293 
0294 //         m_intIn->blockSignals(false);
0295 //         m_intOut->blockSignals(false);
0296 //     } else {
0297 //         /*FIXME: Ideally, these controls should hide away now */
0298 //     }
0299 // }
0300 void KisCurveWidget::setCurve(KisCubicCurve &&curve)
0301 {
0302     m_curve = curve;
0303 }
0304 
0305 QList<QPointF> KisCurveWidget::getPoints() const
0306 {
0307     return m_curve.points();
0308 }