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 }