File indexing completed on 2024-12-22 04:13:10

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-2.0-or-later
0006  */
0007 
0008 
0009 // C++ includes.
0010 
0011 #include <cmath>
0012 #include <cstdlib>
0013 
0014 // Qt includes.
0015 
0016 #include <QPixmap>
0017 #include <QPainter>
0018 #include <QPainterPath>
0019 #include <QPoint>
0020 #include <QPen>
0021 #include <QEvent>
0022 #include <QRect>
0023 #include <QFont>
0024 #include <QFontMetrics>
0025 #include <QMouseEvent>
0026 #include <QKeyEvent>
0027 #include <QPaintEvent>
0028 #include <QList>
0029 #include <QApplication>
0030 
0031 #include <QSpinBox>
0032 
0033 // KDE includes.
0034 
0035 #include <kis_debug.h>
0036 #include <kis_config.h>
0037 #include <klocalizedstring.h>
0038 
0039 #include <kis_signal_compressor.h>
0040 #include <kis_thread_safe_signal_compressor.h>
0041 
0042 
0043 // Local includes.
0044 
0045 #include "widgets/kis_curve_widget.h"
0046 
0047 
0048 #define bounds(x,a,b) (x<a ? a : (x>b ? b :x))
0049 #define MOUSE_AWAY_THRES 15
0050 #define POINT_AREA       1E-4
0051 #define CURVE_AREA       1E-4
0052 
0053 #include "kis_curve_widget_p.h"
0054 
0055 KisCurveWidget::KisCurveWidget(QWidget *parent, Qt::WindowFlags f)
0056         : QWidget(parent, f), d(new KisCurveWidget::Private(this))
0057 {
0058     setObjectName("KisCurveWidget");
0059 
0060     connect(&d->m_modifiedSignalsCompressor, SIGNAL(timeout()), SLOT(notifyModified()));
0061     connect(this, SIGNAL(compressorShouldEmitModified()), SLOT(slotCompressorShouldEmitModified()));
0062 
0063     setMouseTracking(true);
0064     setAutoFillBackground(false);
0065     setAttribute(Qt::WA_OpaquePaintEvent);
0066     setMinimumSize(150, 50);
0067     setMaximumSize(250, 250);
0068 
0069 
0070     setFocusPolicy(Qt::StrongFocus);
0071 }
0072 
0073 KisCurveWidget::~KisCurveWidget()
0074 {
0075     delete d->m_pixmapCache;
0076     delete d;
0077 }
0078 
0079 bool KisCurveWidget::setCurrentPoint(QPointF pt)
0080 {
0081     Q_ASSERT(d->m_grab_point_index >= 0);
0082 
0083     bool needResyncControls = true;
0084     if (d->jumpOverExistingPoints(pt, d->m_grab_point_index)) {
0085         needResyncControls = false;
0086 
0087         d->m_curve.setPoint(d->m_grab_point_index, pt);
0088         d->m_grab_point_index = d->m_curve.points().indexOf(pt);
0089         emit pointSelectedChanged();
0090     } else {
0091         pt = d->m_curve.points()[d->m_grab_point_index];
0092     }
0093 
0094     d->setCurveModified(false);
0095     return needResyncControls;
0096 }
0097 
0098 std::optional<QPointF> KisCurveWidget::currentPoint() const
0099 {
0100     return d->m_grab_point_index >= 0 && d->m_grab_point_index < d->m_curve.points().count() ?
0101                 std::make_optional(d->m_curve.points()[d->m_grab_point_index]) : std::nullopt;
0102 }
0103 
0104 void KisCurveWidget::reset(void)
0105 {
0106     d->m_grab_point_index = -1;
0107     emit pointSelectedChanged();
0108 
0109     //remove total - 2 points.
0110     while (d->m_curve.points().count() - 2 ) {
0111         d->m_curve.removePoint(d->m_curve.points().count() - 2);
0112     }
0113 
0114     d->setCurveModified();
0115 }
0116 
0117 void KisCurveWidget::setPixmap(const QPixmap & pix)
0118 {
0119     d->m_pix = pix;
0120     d->m_pixmapDirty = true;
0121     d->setCurveRepaint();
0122 }
0123 
0124 QPixmap KisCurveWidget::getPixmap()
0125 {
0126     return d->m_pix;
0127 }
0128 
0129 bool KisCurveWidget::pointSelected() const
0130 {
0131     return d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1;
0132 }
0133 
0134 void KisCurveWidget::keyPressEvent(QKeyEvent *e)
0135 {
0136     if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) {
0137         if (d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1) {
0138             //x() find closest point to get focus afterwards
0139             double grab_point_x = d->m_curve.points()[d->m_grab_point_index].x();
0140 
0141             int left_of_grab_point_index = d->m_grab_point_index - 1;
0142             int right_of_grab_point_index = d->m_grab_point_index + 1;
0143             int new_grab_point_index;
0144 
0145             if (fabs(d->m_curve.points()[left_of_grab_point_index].x() - grab_point_x) <
0146                     fabs(d->m_curve.points()[right_of_grab_point_index].x() - grab_point_x)) {
0147                 new_grab_point_index = left_of_grab_point_index;
0148             } else {
0149                 new_grab_point_index = d->m_grab_point_index;
0150             }
0151             d->m_curve.removePoint(d->m_grab_point_index);
0152             d->m_grab_point_index = new_grab_point_index;
0153             emit pointSelectedChanged();
0154             setCursor(Qt::ArrowCursor);
0155             d->setState(ST_NORMAL);
0156         }
0157         e->accept();
0158         d->setCurveModified();
0159     } else if (e->key() == Qt::Key_Escape && d->state() != ST_NORMAL) {
0160         d->m_curve.setPoint(d->m_grab_point_index, QPointF(d->m_grabOriginalX, d->m_grabOriginalY) );
0161         setCursor(Qt::ArrowCursor);
0162         d->setState(ST_NORMAL);
0163 
0164         e->accept();
0165         d->setCurveModified();
0166     } else if ((e->key() == Qt::Key_A || e->key() == Qt::Key_Insert) && d->state() == ST_NORMAL) {
0167         /* FIXME: Lets user choose the hotkeys */
0168         addPointInTheMiddle();
0169         e->accept();
0170     } else
0171         QWidget::keyPressEvent(e);
0172 }
0173 
0174 void KisCurveWidget::addPointInTheMiddle()
0175 {
0176     QPointF pt(0.5, d->m_curve.value(0.5));
0177 
0178     if (!d->jumpOverExistingPoints(pt, -1))
0179         return;
0180 
0181     d->m_grab_point_index = d->m_curve.addPoint(pt);
0182     emit pointSelectedChanged();
0183 
0184     emit shouldFocusIOControls();
0185     d->setCurveModified();
0186 }
0187 
0188 void KisCurveWidget::resizeEvent(QResizeEvent *e)
0189 {
0190     d->m_pixmapDirty = true;
0191     QWidget::resizeEvent(e);
0192 }
0193 
0194 void KisCurveWidget::paintEvent(QPaintEvent *)
0195 {
0196     int    wWidth = width() - 1;
0197     int    wHeight = height() - 1;
0198 
0199 
0200     QPainter p(this);
0201 
0202     // Antialiasing is not a good idea here, because
0203     // the grid will drift one pixel to any side due to rounding of int
0204     // FIXME: let's user tell the last word (in config)
0205     //p.setRenderHint(QPainter::Antialiasing);
0206      QPalette appPalette = QApplication::palette();
0207      p.fillRect(rect(), appPalette.color(QPalette::Base)); // clear out previous paint call results
0208 
0209      // make the entire widget grayed out if it is disabled
0210      if (!this->isEnabled()) {
0211         p.setOpacity(0.2);
0212      }
0213 
0214 
0215 
0216     //  draw background
0217     if (!d->m_pix.isNull()) {
0218         if (d->m_pixmapDirty || !d->m_pixmapCache) {
0219             delete d->m_pixmapCache;
0220             d->m_pixmapCache = new QPixmap(width(), height());
0221             QPainter cachePainter(d->m_pixmapCache);
0222 
0223             cachePainter.scale(1.0*width() / d->m_pix.width(), 1.0*height() / d->m_pix.height());
0224             cachePainter.drawPixmap(0, 0, d->m_pix);
0225             d->m_pixmapDirty = false;
0226         }
0227         p.drawPixmap(0, 0, *d->m_pixmapCache);
0228     }
0229 
0230     d->drawGrid(p, wWidth, wHeight);
0231 
0232     KisConfig cfg(true);
0233     if (cfg.antialiasCurves()) {
0234         p.setRenderHint(QPainter::Antialiasing);
0235     }
0236 
0237     // Draw curve.
0238     double curY;
0239     double normalizedX;
0240     int x;
0241 
0242     QPolygonF poly;
0243 
0244     p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine));
0245     for (x = 0 ; x < wWidth ; x++) {
0246         normalizedX = double(x) / wWidth;
0247         curY = wHeight - d->m_curve.value(normalizedX) * wHeight;
0248 
0249         /**
0250          * Keep in mind that QLineF rounds doubles
0251          * to ints mathematically, not just rounds down
0252          * like in C
0253          */
0254         poly.append(QPointF(x, curY));
0255     }
0256     poly.append(QPointF(x, wHeight - d->m_curve.value(1.0) * wHeight));
0257     p.drawPolyline(poly);
0258 
0259     QPainterPath fillCurvePath;
0260     QPolygonF fillPoly = poly;
0261     fillPoly.append(QPoint(rect().width(), rect().height()));
0262     fillPoly.append(QPointF(0,rect().height()));
0263 
0264     // add a couple points to the edges so it fills in below always
0265 
0266     QColor fillColor = appPalette.color(QPalette::Text);
0267     fillColor.setAlphaF(0.2);
0268 
0269     fillCurvePath.addPolygon(fillPoly);
0270     p.fillPath(fillCurvePath, fillColor);
0271 
0272 
0273 
0274     // Drawing curve handles.
0275     double curveX;
0276     double curveY;
0277     if (!d->m_readOnlyMode) {
0278         for (int i = 0; i < d->m_curve.points().count(); ++i) {
0279             curveX = d->m_curve.points().at(i).x();
0280             curveY = d->m_curve.points().at(i).y();
0281 
0282             if (i == d->m_grab_point_index) {
0283                 // active point is slightly more "bold"
0284                 p.setPen(QPen(appPalette.color(QPalette::Text), 4, Qt::SolidLine));
0285                 p.drawEllipse(QRectF(curveX * wWidth - (d->m_handleSize*0.5),
0286                                      wHeight - (d->m_handleSize*0.5) - curveY * wHeight,
0287                                      d->m_handleSize,
0288                                      d->m_handleSize));
0289 
0290             } else {
0291                 p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine));
0292                 p.drawEllipse(QRectF(curveX * wWidth - (d->m_handleSize*0.5),
0293                                      wHeight - (d->m_handleSize*0.5) - curveY * wHeight,
0294                                      d->m_handleSize,
0295                                      d->m_handleSize));
0296 
0297             }
0298         }
0299     }
0300 
0301     // add border around widget to help contain everything
0302     QPainterPath widgetBoundsPath;
0303     widgetBoundsPath.addRect(rect());
0304     p.strokePath(widgetBoundsPath, appPalette.color(QPalette::Text));
0305 
0306 
0307     p.setOpacity(1.0); // reset to 1.0 in case we were drawing a disabled widget before
0308 }
0309 
0310 void KisCurveWidget::mousePressEvent(QMouseEvent * e)
0311 {
0312     if (d->m_readOnlyMode) return;
0313 
0314     if (e->button() != Qt::LeftButton)
0315         return;
0316 
0317     double x = e->pos().x() / (double)(width() - 1);
0318     double y = 1.0 - e->pos().y() / (double)(height() - 1);
0319 
0320 
0321 
0322     int closest_point_index = d->nearestPointInRange(QPointF(x, y), width(), height());
0323     if (closest_point_index < 0) {
0324         QPointF newPoint(x, y);
0325         if (!d->jumpOverExistingPoints(newPoint, -1))
0326             return;
0327         d->m_grab_point_index = d->m_curve.addPoint(newPoint);
0328         emit pointSelectedChanged();
0329     } else {
0330         d->m_grab_point_index = closest_point_index;
0331         emit pointSelectedChanged();
0332     }
0333 
0334     d->m_grabOriginalX = d->m_curve.points()[d->m_grab_point_index].x();
0335     d->m_grabOriginalY = d->m_curve.points()[d->m_grab_point_index].y();
0336     d->m_grabOffsetX = d->m_curve.points()[d->m_grab_point_index].x() - x;
0337     d->m_grabOffsetY = d->m_curve.points()[d->m_grab_point_index].y() - y;
0338     d->m_curve.setPoint(d->m_grab_point_index, QPointF(x + d->m_grabOffsetX, y + d->m_grabOffsetY));
0339 
0340     d->m_draggedAwayPointIndex = -1;
0341     d->setState(ST_DRAG);
0342 
0343 
0344     d->setCurveModified();
0345 }
0346 
0347 
0348 void KisCurveWidget::mouseReleaseEvent(QMouseEvent *e)
0349 {
0350     if (d->m_readOnlyMode) return;
0351 
0352     if (e->button() != Qt::LeftButton)
0353         return;
0354 
0355     setCursor(Qt::ArrowCursor);
0356     d->setState(ST_NORMAL);
0357 
0358     d->setCurveModified();
0359 }
0360 
0361 
0362 void KisCurveWidget::mouseMoveEvent(QMouseEvent * e)
0363 {
0364     if (d->m_readOnlyMode) return;
0365 
0366     double x = e->pos().x() / (double)(width() - 1);
0367     double y = 1.0 - e->pos().y() / (double)(height() - 1);
0368 
0369     if (d->state() == ST_NORMAL) { // If no point is selected set the cursor shape if on top
0370         int nearestPointIndex = d->nearestPointInRange(QPointF(x, y), width(), height());
0371 
0372         if (nearestPointIndex < 0)
0373             setCursor(Qt::ArrowCursor);
0374         else
0375             setCursor(Qt::CrossCursor);
0376     } else { // Else, drag the selected point
0377         bool crossedHoriz = e->pos().x() - width() > MOUSE_AWAY_THRES ||
0378                             e->pos().x() < -MOUSE_AWAY_THRES;
0379         bool crossedVert =  e->pos().y() - height() > MOUSE_AWAY_THRES ||
0380                             e->pos().y() < -MOUSE_AWAY_THRES;
0381 
0382         bool removePoint = (crossedHoriz || crossedVert);
0383 
0384         if (!removePoint && d->m_draggedAwayPointIndex >= 0) {
0385             // point is no longer dragged away so reinsert it
0386             QPointF newPoint(d->m_draggedAwayPoint);
0387             d->m_grab_point_index = d->m_curve.addPoint(newPoint);
0388             d->m_draggedAwayPointIndex = -1;
0389         }
0390 
0391         if (removePoint &&
0392                 (d->m_draggedAwayPointIndex >= 0))
0393             return;
0394 
0395 
0396         setCursor(Qt::CrossCursor);
0397 
0398         x += d->m_grabOffsetX;
0399         y += d->m_grabOffsetY;
0400 
0401         double leftX;
0402         double rightX;
0403         if (d->m_grab_point_index == 0) {
0404             leftX = 0.0;
0405             if (d->m_curve.points().count() > 1)
0406                 rightX = d->m_curve.points()[d->m_grab_point_index + 1].x() - POINT_AREA;
0407             else
0408                 rightX = 1.0;
0409         } else if (d->m_grab_point_index == d->m_curve.points().count() - 1) {
0410             leftX = d->m_curve.points()[d->m_grab_point_index - 1].x() + POINT_AREA;
0411             rightX = 1.0;
0412         } else {
0413             Q_ASSERT(d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1);
0414 
0415             // the 1E-4 addition so we can grab the dot later.
0416             leftX = d->m_curve.points()[d->m_grab_point_index - 1].x() + POINT_AREA;
0417             rightX = d->m_curve.points()[d->m_grab_point_index + 1].x() - POINT_AREA;
0418         }
0419 
0420         x = bounds(x, leftX, rightX);
0421         y = bounds(y, 0., 1.);
0422 
0423         d->m_curve.setPoint(d->m_grab_point_index, QPointF(x, y));
0424 
0425         if (removePoint && d->m_curve.points().count() > 2) {
0426             d->m_draggedAwayPoint = d->m_curve.points()[d->m_grab_point_index];
0427             d->m_draggedAwayPointIndex = d->m_grab_point_index;
0428             d->m_curve.removePoint(d->m_grab_point_index);
0429             d->m_grab_point_index = bounds(d->m_grab_point_index, 0, d->m_curve.points().count() - 1);
0430             emit pointSelectedChanged();
0431         }
0432 
0433         d->setCurveModified();
0434     }
0435 }
0436 
0437 KisCubicCurve KisCurveWidget::curve()
0438 {
0439     return d->m_curve;
0440 }
0441 
0442 void KisCurveWidget::setCurve(KisCubicCurve inlist)
0443 {
0444     d->m_curve = inlist;
0445     d->m_grab_point_index = qBound(0, d->m_grab_point_index, d->m_curve.points().count() - 1);
0446     d->setCurveModified();
0447     emit pointSelectedChanged();
0448 }
0449 
0450 void KisCurveWidget::leaveEvent(QEvent *)
0451 {
0452 }
0453 
0454 void KisCurveWidget::notifyModified()
0455 {
0456     emit modified();
0457     emit curveChanged(d->m_curve);
0458 }
0459 
0460 void KisCurveWidget::slotCompressorShouldEmitModified()
0461 {
0462     d->m_modifiedSignalsCompressor.start();
0463 }