Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_curve_widget.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2005 C. Boemann <cbo@boemann.dk>
3 * SPDX-FileCopyrightText: 2009 Dmitry Kazakov <dimula73@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8
9// C++ includes.
10
11#include <cmath>
12#include <cstdlib>
13
14// Qt includes.
15
16#include <QPixmap>
17#include <QPainter>
18#include <QPainterPath>
19#include <QPoint>
20#include <QPen>
21#include <QEvent>
22#include <QFont>
23#include <QFontMetrics>
24#include <QMouseEvent>
25#include <QKeyEvent>
26#include <QPaintEvent>
27#include <QApplication>
28
29#include <QSpinBox>
30
31// KDE includes.
32
33#include <kis_debug.h>
34#include <kis_config.h>
35#include <klocalizedstring.h>
36
39
40
41// Local includes.
42
44
45
46#define bounds(x,a,b) (x<a ? a : (x>b ? b :x))
47#define MOUSE_AWAY_THRES 15
48#define POINT_AREA 1E-4
49#define CURVE_AREA 1E-4
50
51#include "kis_curve_widget_p.h"
52
53KisCurveWidget::KisCurveWidget(QWidget *parent, Qt::WindowFlags f)
54 : QWidget(parent, f), d(new KisCurveWidget::Private(this))
55{
56 setObjectName("KisCurveWidget");
57
58 connect(&d->m_modifiedSignalsCompressor, SIGNAL(timeout()), SLOT(notifyModified()));
59 connect(this, SIGNAL(compressorShouldEmitModified()), SLOT(slotCompressorShouldEmitModified()));
60
61 setMouseTracking(true);
62 setAutoFillBackground(false);
63 setAttribute(Qt::WA_OpaquePaintEvent);
64 setMinimumSize(150, 50);
65 setMaximumSize(250, 250);
66
67 // Curves widgets don't have context menus. Setting this prevents any
68 // long-presses from delaying inputs, see KisLongPressEventFilter.cpp.
69 setContextMenuPolicy(Qt::PreventContextMenu);
70
71 setFocusPolicy(Qt::StrongFocus);
72}
73
75{
76 delete d->m_pixmapCache;
77 delete d;
78}
79
80bool KisCurveWidget::setCurrentPoint(const QPointF &position, bool setAsCorner)
81{
82 Q_ASSERT(d->m_grab_point_index >= 0);
83
84 bool needResyncControls = true;
85 bool isCorner;
86
87 if (d->m_globalPointConstrain == PointConstrain_None) {
88 d->m_curve.setPointAsCorner(d->m_grab_point_index, setAsCorner);
89 isCorner = setAsCorner;
90 } else {
91 isCorner = d->m_globalPointConstrain == PointConstrain_AlwaysCorner;
92 }
93
94 QPointF newPosition(position);
95
96 if (d->jumpOverExistingPoints(newPosition, d->m_grab_point_index)) {
97 needResyncControls = false;
98 d->m_curve.setPointPosition(d->m_grab_point_index, newPosition);
99 d->m_grab_point_index = d->m_curve.curvePoints().indexOf(
100 KisCubicCurvePoint(newPosition, isCorner)
101 );
102 Q_EMIT pointSelectedChanged();
103 }
104
105 d->setCurveModified(false);
106 return needResyncControls;
107}
108
109bool KisCurveWidget::setCurrentPointPosition(const QPointF &position)
110{
111 Q_ASSERT(d->m_grab_point_index >= 0);
112
113 return setCurrentPoint(position, d->m_curve.curvePoints()[d->m_grab_point_index].isSetAsCorner());
114}
115
117{
118 Q_ASSERT(d->m_grab_point_index >= 0);
119
120 // Setting the point corner flag is not allowed if there is some global constrain set
121 if (d->m_globalPointConstrain != PointConstrain_None) {
122 return false;
123 }
124
125 if (setAsCorner != d->m_curve.curvePoints()[d->m_grab_point_index].isSetAsCorner()) {
126 d->m_curve.setPointAsCorner(d->m_grab_point_index, setAsCorner);
127 d->setCurveModified(false);
128 }
129
130 return false;
131}
132
134{
135 if (d->m_globalPointConstrain == constrain) {
136 return;
137 }
138
139 d->m_globalPointConstrain = constrain;
140 d->applyGlobalPointConstrain();
141
142 d->setCurveModified(false);
143}
144
145std::optional<KisCubicCurvePoint> KisCurveWidget::currentPoint() const
146{
147 return d->m_grab_point_index >= 0 && d->m_grab_point_index < d->m_curve.curvePoints().count()
148 ? std::make_optional(d->m_curve.curvePoints()[d->m_grab_point_index])
149 : std::nullopt;
150}
151
152std::optional<QPointF> KisCurveWidget::currentPointPosition() const
153{
154 return d->m_grab_point_index >= 0 && d->m_grab_point_index < d->m_curve.curvePoints().count()
155 ? std::make_optional(d->m_curve.curvePoints()[d->m_grab_point_index].position())
156 : std::nullopt;
157}
158
160{
161 return d->m_grab_point_index >= 0 && d->m_grab_point_index < d->m_curve.curvePoints().count()
162 ? std::make_optional(d->m_curve.curvePoints()[d->m_grab_point_index].isSetAsCorner())
163 : std::nullopt;
164}
165
167{
168 return d->m_globalPointConstrain;
169}
170
172{
173 d->m_grab_point_index = -1;
174 Q_EMIT pointSelectedChanged();
175
176 //remove total - 2 points.
177 while (d->m_curve.curvePoints().count() - 2 ) {
178 d->m_curve.removePoint(d->m_curve.curvePoints().count() - 2);
179 }
180
181 d->setCurveModified();
182}
183
184void KisCurveWidget::setPixmap(const QPixmap & pix)
185{
186 d->m_pix = pix;
187 d->m_pixmapDirty = true;
188 d->setCurveRepaint();
189}
190
192{
193 return d->m_pix;
194}
195
197{
198 return d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.curvePoints().count();
199}
200
202{
203 if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) {
204 if (d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.curvePoints().count() - 1) {
205 //x() find closest point to get focus afterwards
206 double grab_point_x = d->m_curve.curvePoints()[d->m_grab_point_index].x();
207
208 int left_of_grab_point_index = d->m_grab_point_index - 1;
209 int right_of_grab_point_index = d->m_grab_point_index + 1;
210 int new_grab_point_index;
211
212 if (fabs(d->m_curve.curvePoints()[left_of_grab_point_index].x() - grab_point_x) <
213 fabs(d->m_curve.curvePoints()[right_of_grab_point_index].x() - grab_point_x)) {
214 new_grab_point_index = left_of_grab_point_index;
215 } else {
216 new_grab_point_index = d->m_grab_point_index;
217 }
218 d->m_curve.removePoint(d->m_grab_point_index);
219 d->m_grab_point_index = new_grab_point_index;
220 Q_EMIT pointSelectedChanged();
221 setCursor(Qt::ArrowCursor);
222 d->setState(ST_NORMAL);
223 }
224 e->accept();
225 d->setCurveModified();
226 } else if (e->key() == Qt::Key_Escape && d->state() != ST_NORMAL) {
227 d->m_curve.setPointPosition(d->m_grab_point_index, QPointF(d->m_grabOriginalX, d->m_grabOriginalY) );
228 setCursor(Qt::ArrowCursor);
229 d->setState(ST_NORMAL);
230
231 e->accept();
232 d->setCurveModified();
233 } else if ((e->key() == Qt::Key_A || e->key() == Qt::Key_Insert) && d->state() == ST_NORMAL) {
234 /* FIXME: Lets user choose the hotkeys */
236 e->accept();
237 } else if (e->key() == Qt::Key_S &&
238 d->m_globalPointConstrain == PointConstrain_None &&
239 pointSelected() &&
240 d->state() == ST_NORMAL) {
241 /* FIXME: Lets user choose the hotkeys */
243 e->accept();
244 } else
245 QWidget::keyPressEvent(e);
246}
247
249{
250 QPointF position(0.5, d->m_curve.value(0.5));
251
252 if (!d->jumpOverExistingPoints(position, -1))
253 return;
254
255 const bool setAsCorner = d->m_globalPointConstrain == PointConstrain_AlwaysCorner;
256
257 d->m_grab_point_index = d->m_curve.addPoint(position, setAsCorner);
258 Q_EMIT pointSelectedChanged();
259
260 Q_EMIT shouldFocusIOControls();
261 d->setCurveModified();
262}
263
264void KisCurveWidget::resizeEvent(QResizeEvent *e)
265{
266 d->m_pixmapDirty = true;
267 QWidget::resizeEvent(e);
268}
269
270void KisCurveWidget::paintEvent(QPaintEvent *)
271{
272 int wWidth = width() - 1;
273 int wHeight = height() - 1;
274
275
276 QPainter p(this);
277
278 // Antialiasing is not a good idea here, because
279 // the grid will drift one pixel to any side due to rounding of int
280 // FIXME: let's user tell the last word (in config)
281 //p.setRenderHint(QPainter::Antialiasing);
282 QPalette appPalette = QApplication::palette();
283 p.fillRect(rect(), appPalette.color(QPalette::Base)); // clear out previous paint call results
284
285 // make the entire widget grayed out if it is disabled
286 if (!this->isEnabled()) {
287 p.setOpacity(0.2);
288 }
289
290
291
292 // draw background
293 if (!d->m_pix.isNull()) {
294 if (d->m_pixmapDirty || !d->m_pixmapCache) {
295 delete d->m_pixmapCache;
296 d->m_pixmapCache = new QPixmap(width(), height());
297 QPainter cachePainter(d->m_pixmapCache);
298
299 cachePainter.scale(1.0*width() / d->m_pix.width(), 1.0*height() / d->m_pix.height());
300 cachePainter.drawPixmap(0, 0, d->m_pix);
301 d->m_pixmapDirty = false;
302 }
303 p.drawPixmap(0, 0, *d->m_pixmapCache);
304 }
305
306 d->drawGrid(p, wWidth, wHeight);
307
308 KisConfig cfg(true);
309 if (cfg.antialiasCurves()) {
310 p.setRenderHint(QPainter::Antialiasing);
311 }
312
313 // Draw curve.
314 double curY;
315 double normalizedX;
316 int x;
317
318 QPolygonF poly;
319
320 p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine));
321 for (x = 0 ; x < wWidth ; x++) {
322 normalizedX = double(x) / wWidth;
323 curY = wHeight - d->m_curve.value(normalizedX) * wHeight;
324
330 poly.append(QPointF(x, curY));
331 }
332 poly.append(QPointF(x, wHeight - d->m_curve.value(1.0) * wHeight));
333 p.drawPolyline(poly);
334
335 QPainterPath fillCurvePath;
336 QPolygonF fillPoly = poly;
337 fillPoly.append(QPoint(rect().width(), rect().height()));
338 fillPoly.append(QPointF(0,rect().height()));
339
340 // add a couple points to the edges so it fills in below always
341
342 QColor fillColor = appPalette.color(QPalette::Text);
343 fillColor.setAlphaF(0.2);
344
345 fillCurvePath.addPolygon(fillPoly);
346 p.fillPath(fillCurvePath, fillColor);
347
348
349
350 // Drawing curve handles.
351 if (!d->m_readOnlyMode) {
352 const qreal halfHandleSize = d->m_handleSize * 0.5;
353
354 for (int i = 0; i < d->m_curve.curvePoints().count(); ++i) {
355 const KisCubicCurvePoint &point = d->m_curve.points().at(i);
356
357 if (i == d->m_grab_point_index) {
358 // active point is slightly more "bold"
359 p.setPen(QPen(appPalette.color(QPalette::Text), 4, Qt::SolidLine));
360 } else {
361 p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine));
362 }
363
364 const QPointF handleCenter(point.x() * wWidth, wHeight - point.y() * wHeight);
365
366 if (point.isSetAsCorner()) {
367 QPolygonF rhombusHandle;
368 rhombusHandle.append(QPointF(handleCenter.x() - halfHandleSize, handleCenter.y()));
369 rhombusHandle.append(QPointF(handleCenter.x(), handleCenter.y() - halfHandleSize));
370 rhombusHandle.append(QPointF(handleCenter.x() + halfHandleSize, handleCenter.y()));
371 rhombusHandle.append(QPointF(handleCenter.x(), handleCenter.y() + halfHandleSize));
372 p.drawPolygon(rhombusHandle);
373 } else {
374 p.drawEllipse(handleCenter, halfHandleSize, halfHandleSize);
375 }
376 }
377 }
378
379 // add border around widget to help contain everything
380 QPainterPath widgetBoundsPath;
381 widgetBoundsPath.addRect(rect());
382 p.strokePath(widgetBoundsPath, appPalette.color(QPalette::Text));
383
384
385 p.setOpacity(1.0); // reset to 1.0 in case we were drawing a disabled widget before
386}
387
389{
390 if (d->m_readOnlyMode) return;
391
392 if (e->button() != Qt::LeftButton)
393 return;
394
395 double x = e->pos().x() / (double)(width() - 1);
396 double y = 1.0 - e->pos().y() / (double)(height() - 1);
397
398
399
400 int closest_point_index = d->nearestPointInRange(QPointF(x, y), width(), height());
401 if (closest_point_index < 0) {
402 QPointF newPoint(x, y);
403 if (!d->jumpOverExistingPoints(newPoint, -1))
404 return;
405
406 const bool setAsCorner = d->m_globalPointConstrain == PointConstrain_AlwaysCorner;
407 d->m_grab_point_index = d->m_curve.addPoint(newPoint, setAsCorner);
408 Q_EMIT pointSelectedChanged();
409 } else {
410 d->m_grab_point_index = closest_point_index;
411 Q_EMIT pointSelectedChanged();
412 }
413
414 const KisCubicCurvePoint &currentPoint = d->m_curve.curvePoints()[d->m_grab_point_index];
415
416 d->m_grabOriginalX = currentPoint.x();
417 d->m_grabOriginalY = currentPoint.y();
418 d->m_grabOffsetX = currentPoint.x() - x;
419 d->m_grabOffsetY = currentPoint.y() - y;
420 if (e->modifiers().testFlag(Qt::ControlModifier) && d->m_globalPointConstrain == PointConstrain_None) {
421 d->m_curve.setPointAsCorner(d->m_grab_point_index, !currentPoint.isSetAsCorner());
422 }
423 d->m_curve.setPointPosition(d->m_grab_point_index, QPointF(x + d->m_grabOffsetX, y + d->m_grabOffsetY));
424
425 d->m_draggedAwayPointIndex = -1;
426 d->setState(ST_DRAG);
427
428
429 d->setCurveModified();
430}
431
432
434{
435 if (d->m_readOnlyMode) return;
436
437 if (e->button() != Qt::LeftButton)
438 return;
439
440 setCursor(Qt::ArrowCursor);
441 d->setState(ST_NORMAL);
442
443 d->setCurveModified();
444}
445
446
448{
449 if (d->m_readOnlyMode) return;
450
451 double x = e->pos().x() / (double)(width() - 1);
452 double y = 1.0 - e->pos().y() / (double)(height() - 1);
453
454 if (d->state() == ST_NORMAL) { // If no point is selected set the cursor shape if on top
455 int nearestPointIndex = d->nearestPointInRange(QPointF(x, y), width(), height());
456
457 if (nearestPointIndex < 0)
458 setCursor(Qt::ArrowCursor);
459 else
460 setCursor(Qt::CrossCursor);
461 } else { // Else, drag the selected point
462 bool crossedHoriz = e->pos().x() - width() > MOUSE_AWAY_THRES ||
463 e->pos().x() < -MOUSE_AWAY_THRES;
464 bool crossedVert = e->pos().y() - height() > MOUSE_AWAY_THRES ||
465 e->pos().y() < -MOUSE_AWAY_THRES;
466
467 bool removePoint = (crossedHoriz || crossedVert);
468
469 if (!removePoint && d->m_draggedAwayPointIndex >= 0) {
470 // point is no longer dragged away so reinsert it
471 KisCubicCurvePoint newPoint(d->m_draggedAwayPoint);
472 d->m_grab_point_index = d->m_curve.addPoint(newPoint);
473 d->m_draggedAwayPointIndex = -1;
474 }
475
476 if (removePoint &&
477 (d->m_draggedAwayPointIndex >= 0))
478 return;
479
480
481 setCursor(Qt::CrossCursor);
482
483 x += d->m_grabOffsetX;
484 y += d->m_grabOffsetY;
485
486 double leftX;
487 double rightX;
488 if (d->m_grab_point_index == 0) {
489 leftX = 0.0;
490 if (d->m_curve.curvePoints().count() > 1)
491 rightX = d->m_curve.curvePoints()[d->m_grab_point_index + 1].x() - POINT_AREA;
492 else
493 rightX = 1.0;
494 } else if (d->m_grab_point_index == d->m_curve.curvePoints().count() - 1) {
495 leftX = d->m_curve.curvePoints()[d->m_grab_point_index - 1].x() + POINT_AREA;
496 rightX = 1.0;
497 } else {
498 Q_ASSERT(d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.curvePoints().count() - 1);
499
500 // the 1E-4 addition so we can grab the dot later.
501 leftX = d->m_curve.curvePoints()[d->m_grab_point_index - 1].x() + POINT_AREA;
502 rightX = d->m_curve.curvePoints()[d->m_grab_point_index + 1].x() - POINT_AREA;
503 }
504
505 x = bounds(x, leftX, rightX);
506 y = bounds(y, 0., 1.);
507
508 d->m_curve.setPointPosition(d->m_grab_point_index, QPointF(x, y));
509
510 if (removePoint && d->m_curve.curvePoints().count() > 2) {
511 d->m_draggedAwayPoint = d->m_curve.curvePoints()[d->m_grab_point_index];
512 d->m_draggedAwayPointIndex = d->m_grab_point_index;
513 d->m_curve.removePoint(d->m_grab_point_index);
514 d->m_grab_point_index = bounds(d->m_grab_point_index, 0, d->m_curve.curvePoints().count() - 1);
515 Q_EMIT pointSelectedChanged();
516 }
517
518 d->setCurveModified();
519 }
520}
521
523{
524 return d->m_curve;
525}
526
528{
529 d->m_curve = inlist;
530 d->m_grab_point_index = qBound(0, d->m_grab_point_index, d->m_curve.curvePoints().count() - 1);
531 d->applyGlobalPointConstrain();
532 d->setCurveModified();
533 Q_EMIT pointSelectedChanged();
534}
535
537{
538}
539
541{
542 Q_EMIT modified();
543 Q_EMIT curveChanged(d->m_curve);
544}
545
547{
548 d->m_modifiedSignalsCompressor.start();
549}
const Params2D p
bool antialiasCurves(bool defaultValue=false) const
bool isSetAsCorner() const
void mousePressEvent(QMouseEvent *e) override
void setPixmap(const QPixmap &pix)
void paintEvent(QPaintEvent *) override
void leaveEvent(QEvent *) override
bool setCurrentPoint(const QPointF &position, bool setAsCorner)
void modified(void)
Private *const d
std::optional< bool > isCurrentPointSetAsCorner() const
void mouseReleaseEvent(QMouseEvent *e) override
bool setCurrentPointAsCorner(bool setAsCorner)
PointConstrain globalPointConstrain() const
void curveChanged(const KisCubicCurve &)
~KisCurveWidget() override
void keyPressEvent(QKeyEvent *) override
bool setCurrentPointPosition(const QPointF &position)
KisCubicCurve curve()
void pointSelectedChanged()
void slotCompressorShouldEmitModified()
KisCurveWidget(QWidget *parent=nullptr, Qt::WindowFlags f=Qt::WindowFlags())
void setGlobalPointConstrain(PointConstrain constrain)
std::optional< QPointF > currentPointPosition() const
void compressorShouldEmitModified()
void shouldFocusIOControls()
void resizeEvent(QResizeEvent *e) override
void setCurve(KisCubicCurve inlist)
void mouseMoveEvent(QMouseEvent *e) override
std::optional< KisCubicCurvePoint > currentPoint() const
#define MOUSE_AWAY_THRES
#define POINT_AREA
#define bounds(x, a, b)
@ ST_DRAG
@ ST_NORMAL