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()));
60
61 setMouseTracking(true);
62 setAutoFillBackground(false);
63 setAttribute(Qt::WA_OpaquePaintEvent);
64 setMinimumSize(150, 50);
65 setMaximumSize(250, 250);
66
67
68 setFocusPolicy(Qt::StrongFocus);
69}
70
72{
73 delete d->m_pixmapCache;
74 delete d;
75}
76
77bool KisCurveWidget::setCurrentPoint(const QPointF &position, bool setAsCorner)
78{
79 Q_ASSERT(d->m_grab_point_index >= 0);
80
81 bool needResyncControls = true;
82 bool isCorner;
83
84 if (d->m_globalPointConstrain == PointConstrain_None) {
85 d->m_curve.setPointAsCorner(d->m_grab_point_index, setAsCorner);
86 isCorner = setAsCorner;
87 } else {
88 isCorner = d->m_globalPointConstrain == PointConstrain_AlwaysCorner;
89 }
90
91 QPointF newPosition(position);
92
93 if (d->jumpOverExistingPoints(newPosition, d->m_grab_point_index)) {
94 needResyncControls = false;
95 d->m_curve.setPointPosition(d->m_grab_point_index, newPosition);
96 d->m_grab_point_index = d->m_curve.curvePoints().indexOf(
97 KisCubicCurvePoint(newPosition, isCorner)
98 );
99 Q_EMIT pointSelectedChanged();
100 }
101
102 d->setCurveModified(false);
103 return needResyncControls;
104}
105
106bool KisCurveWidget::setCurrentPointPosition(const QPointF &position)
107{
108 Q_ASSERT(d->m_grab_point_index >= 0);
109
110 return setCurrentPoint(position, d->m_curve.curvePoints()[d->m_grab_point_index].isSetAsCorner());
111}
112
114{
115 Q_ASSERT(d->m_grab_point_index >= 0);
116
117 // Setting the point corner flag is not allowed if there is some global constrain set
118 if (d->m_globalPointConstrain != PointConstrain_None) {
119 return false;
120 }
121
122 if (setAsCorner != d->m_curve.curvePoints()[d->m_grab_point_index].isSetAsCorner()) {
123 d->m_curve.setPointAsCorner(d->m_grab_point_index, setAsCorner);
124 d->setCurveModified(false);
125 }
126
127 return false;
128}
129
131{
132 if (d->m_globalPointConstrain == constrain) {
133 return;
134 }
135
136 d->m_globalPointConstrain = constrain;
137 d->applyGlobalPointConstrain();
138
139 d->setCurveModified(false);
140}
141
142std::optional<KisCubicCurvePoint> KisCurveWidget::currentPoint() const
143{
144 return d->m_grab_point_index >= 0 && d->m_grab_point_index < d->m_curve.curvePoints().count()
145 ? std::make_optional(d->m_curve.curvePoints()[d->m_grab_point_index])
146 : std::nullopt;
147}
148
149std::optional<QPointF> KisCurveWidget::currentPointPosition() const
150{
151 return d->m_grab_point_index >= 0 && d->m_grab_point_index < d->m_curve.curvePoints().count()
152 ? std::make_optional(d->m_curve.curvePoints()[d->m_grab_point_index].position())
153 : std::nullopt;
154}
155
157{
158 return d->m_grab_point_index >= 0 && d->m_grab_point_index < d->m_curve.curvePoints().count()
159 ? std::make_optional(d->m_curve.curvePoints()[d->m_grab_point_index].isSetAsCorner())
160 : std::nullopt;
161}
162
164{
165 return d->m_globalPointConstrain;
166}
167
169{
170 d->m_grab_point_index = -1;
171 Q_EMIT pointSelectedChanged();
172
173 //remove total - 2 points.
174 while (d->m_curve.curvePoints().count() - 2 ) {
175 d->m_curve.removePoint(d->m_curve.curvePoints().count() - 2);
176 }
177
178 d->setCurveModified();
179}
180
181void KisCurveWidget::setPixmap(const QPixmap & pix)
182{
183 d->m_pix = pix;
184 d->m_pixmapDirty = true;
185 d->setCurveRepaint();
186}
187
189{
190 return d->m_pix;
191}
192
194{
195 return d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.curvePoints().count();
196}
197
199{
200 if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) {
201 if (d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.curvePoints().count() - 1) {
202 //x() find closest point to get focus afterwards
203 double grab_point_x = d->m_curve.curvePoints()[d->m_grab_point_index].x();
204
205 int left_of_grab_point_index = d->m_grab_point_index - 1;
206 int right_of_grab_point_index = d->m_grab_point_index + 1;
207 int new_grab_point_index;
208
209 if (fabs(d->m_curve.curvePoints()[left_of_grab_point_index].x() - grab_point_x) <
210 fabs(d->m_curve.curvePoints()[right_of_grab_point_index].x() - grab_point_x)) {
211 new_grab_point_index = left_of_grab_point_index;
212 } else {
213 new_grab_point_index = d->m_grab_point_index;
214 }
215 d->m_curve.removePoint(d->m_grab_point_index);
216 d->m_grab_point_index = new_grab_point_index;
217 Q_EMIT pointSelectedChanged();
218 setCursor(Qt::ArrowCursor);
219 d->setState(ST_NORMAL);
220 }
221 e->accept();
222 d->setCurveModified();
223 } else if (e->key() == Qt::Key_Escape && d->state() != ST_NORMAL) {
224 d->m_curve.setPointPosition(d->m_grab_point_index, QPointF(d->m_grabOriginalX, d->m_grabOriginalY) );
225 setCursor(Qt::ArrowCursor);
226 d->setState(ST_NORMAL);
227
228 e->accept();
229 d->setCurveModified();
230 } else if ((e->key() == Qt::Key_A || e->key() == Qt::Key_Insert) && d->state() == ST_NORMAL) {
231 /* FIXME: Lets user choose the hotkeys */
233 e->accept();
234 } else if (e->key() == Qt::Key_S &&
235 d->m_globalPointConstrain == PointConstrain_None &&
236 pointSelected() &&
237 d->state() == ST_NORMAL) {
238 /* FIXME: Lets user choose the hotkeys */
240 e->accept();
241 } else
242 QWidget::keyPressEvent(e);
243}
244
246{
247 QPointF position(0.5, d->m_curve.value(0.5));
248
249 if (!d->jumpOverExistingPoints(position, -1))
250 return;
251
252 const bool setAsCorner = d->m_globalPointConstrain == PointConstrain_AlwaysCorner;
253
254 d->m_grab_point_index = d->m_curve.addPoint(position, setAsCorner);
255 Q_EMIT pointSelectedChanged();
256
257 Q_EMIT shouldFocusIOControls();
258 d->setCurveModified();
259}
260
261void KisCurveWidget::resizeEvent(QResizeEvent *e)
262{
263 d->m_pixmapDirty = true;
264 QWidget::resizeEvent(e);
265}
266
267void KisCurveWidget::paintEvent(QPaintEvent *)
268{
269 int wWidth = width() - 1;
270 int wHeight = height() - 1;
271
272
273 QPainter p(this);
274
275 // Antialiasing is not a good idea here, because
276 // the grid will drift one pixel to any side due to rounding of int
277 // FIXME: let's user tell the last word (in config)
278 //p.setRenderHint(QPainter::Antialiasing);
279 QPalette appPalette = QApplication::palette();
280 p.fillRect(rect(), appPalette.color(QPalette::Base)); // clear out previous paint call results
281
282 // make the entire widget grayed out if it is disabled
283 if (!this->isEnabled()) {
284 p.setOpacity(0.2);
285 }
286
287
288
289 // draw background
290 if (!d->m_pix.isNull()) {
291 if (d->m_pixmapDirty || !d->m_pixmapCache) {
292 delete d->m_pixmapCache;
293 d->m_pixmapCache = new QPixmap(width(), height());
294 QPainter cachePainter(d->m_pixmapCache);
295
296 cachePainter.scale(1.0*width() / d->m_pix.width(), 1.0*height() / d->m_pix.height());
297 cachePainter.drawPixmap(0, 0, d->m_pix);
298 d->m_pixmapDirty = false;
299 }
300 p.drawPixmap(0, 0, *d->m_pixmapCache);
301 }
302
303 d->drawGrid(p, wWidth, wHeight);
304
305 KisConfig cfg(true);
306 if (cfg.antialiasCurves()) {
307 p.setRenderHint(QPainter::Antialiasing);
308 }
309
310 // Draw curve.
311 double curY;
312 double normalizedX;
313 int x;
314
315 QPolygonF poly;
316
317 p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine));
318 for (x = 0 ; x < wWidth ; x++) {
319 normalizedX = double(x) / wWidth;
320 curY = wHeight - d->m_curve.value(normalizedX) * wHeight;
321
327 poly.append(QPointF(x, curY));
328 }
329 poly.append(QPointF(x, wHeight - d->m_curve.value(1.0) * wHeight));
330 p.drawPolyline(poly);
331
332 QPainterPath fillCurvePath;
333 QPolygonF fillPoly = poly;
334 fillPoly.append(QPoint(rect().width(), rect().height()));
335 fillPoly.append(QPointF(0,rect().height()));
336
337 // add a couple points to the edges so it fills in below always
338
339 QColor fillColor = appPalette.color(QPalette::Text);
340 fillColor.setAlphaF(0.2);
341
342 fillCurvePath.addPolygon(fillPoly);
343 p.fillPath(fillCurvePath, fillColor);
344
345
346
347 // Drawing curve handles.
348 if (!d->m_readOnlyMode) {
349 const qreal halfHandleSize = d->m_handleSize * 0.5;
350
351 for (int i = 0; i < d->m_curve.curvePoints().count(); ++i) {
352 const KisCubicCurvePoint &point = d->m_curve.points().at(i);
353
354 if (i == d->m_grab_point_index) {
355 // active point is slightly more "bold"
356 p.setPen(QPen(appPalette.color(QPalette::Text), 4, Qt::SolidLine));
357 } else {
358 p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine));
359 }
360
361 const QPointF handleCenter(point.x() * wWidth, wHeight - point.y() * wHeight);
362
363 if (point.isSetAsCorner()) {
364 QPolygonF rhombusHandle;
365 rhombusHandle.append(QPointF(handleCenter.x() - halfHandleSize, handleCenter.y()));
366 rhombusHandle.append(QPointF(handleCenter.x(), handleCenter.y() - halfHandleSize));
367 rhombusHandle.append(QPointF(handleCenter.x() + halfHandleSize, handleCenter.y()));
368 rhombusHandle.append(QPointF(handleCenter.x(), handleCenter.y() + halfHandleSize));
369 p.drawPolygon(rhombusHandle);
370 } else {
371 p.drawEllipse(handleCenter, halfHandleSize, halfHandleSize);
372 }
373 }
374 }
375
376 // add border around widget to help contain everything
377 QPainterPath widgetBoundsPath;
378 widgetBoundsPath.addRect(rect());
379 p.strokePath(widgetBoundsPath, appPalette.color(QPalette::Text));
380
381
382 p.setOpacity(1.0); // reset to 1.0 in case we were drawing a disabled widget before
383}
384
386{
387 if (d->m_readOnlyMode) return;
388
389 if (e->button() != Qt::LeftButton)
390 return;
391
392 double x = e->pos().x() / (double)(width() - 1);
393 double y = 1.0 - e->pos().y() / (double)(height() - 1);
394
395
396
397 int closest_point_index = d->nearestPointInRange(QPointF(x, y), width(), height());
398 if (closest_point_index < 0) {
399 QPointF newPoint(x, y);
400 if (!d->jumpOverExistingPoints(newPoint, -1))
401 return;
402
403 const bool setAsCorner = d->m_globalPointConstrain == PointConstrain_AlwaysCorner;
404 d->m_grab_point_index = d->m_curve.addPoint(newPoint, setAsCorner);
405 Q_EMIT pointSelectedChanged();
406 } else {
407 d->m_grab_point_index = closest_point_index;
408 Q_EMIT pointSelectedChanged();
409 }
410
411 const KisCubicCurvePoint &currentPoint = d->m_curve.curvePoints()[d->m_grab_point_index];
412
413 d->m_grabOriginalX = currentPoint.x();
414 d->m_grabOriginalY = currentPoint.y();
415 d->m_grabOffsetX = currentPoint.x() - x;
416 d->m_grabOffsetY = currentPoint.y() - y;
417 if (e->modifiers().testFlag(Qt::ControlModifier) && d->m_globalPointConstrain == PointConstrain_None) {
418 d->m_curve.setPointAsCorner(d->m_grab_point_index, !currentPoint.isSetAsCorner());
419 }
420 d->m_curve.setPointPosition(d->m_grab_point_index, QPointF(x + d->m_grabOffsetX, y + d->m_grabOffsetY));
421
422 d->m_draggedAwayPointIndex = -1;
423 d->setState(ST_DRAG);
424
425
426 d->setCurveModified();
427}
428
429
431{
432 if (d->m_readOnlyMode) return;
433
434 if (e->button() != Qt::LeftButton)
435 return;
436
437 setCursor(Qt::ArrowCursor);
438 d->setState(ST_NORMAL);
439
440 d->setCurveModified();
441}
442
443
445{
446 if (d->m_readOnlyMode) return;
447
448 double x = e->pos().x() / (double)(width() - 1);
449 double y = 1.0 - e->pos().y() / (double)(height() - 1);
450
451 if (d->state() == ST_NORMAL) { // If no point is selected set the cursor shape if on top
452 int nearestPointIndex = d->nearestPointInRange(QPointF(x, y), width(), height());
453
454 if (nearestPointIndex < 0)
455 setCursor(Qt::ArrowCursor);
456 else
457 setCursor(Qt::CrossCursor);
458 } else { // Else, drag the selected point
459 bool crossedHoriz = e->pos().x() - width() > MOUSE_AWAY_THRES ||
460 e->pos().x() < -MOUSE_AWAY_THRES;
461 bool crossedVert = e->pos().y() - height() > MOUSE_AWAY_THRES ||
462 e->pos().y() < -MOUSE_AWAY_THRES;
463
464 bool removePoint = (crossedHoriz || crossedVert);
465
466 if (!removePoint && d->m_draggedAwayPointIndex >= 0) {
467 // point is no longer dragged away so reinsert it
468 KisCubicCurvePoint newPoint(d->m_draggedAwayPoint);
469 d->m_grab_point_index = d->m_curve.addPoint(newPoint);
470 d->m_draggedAwayPointIndex = -1;
471 }
472
473 if (removePoint &&
474 (d->m_draggedAwayPointIndex >= 0))
475 return;
476
477
478 setCursor(Qt::CrossCursor);
479
480 x += d->m_grabOffsetX;
481 y += d->m_grabOffsetY;
482
483 double leftX;
484 double rightX;
485 if (d->m_grab_point_index == 0) {
486 leftX = 0.0;
487 if (d->m_curve.curvePoints().count() > 1)
488 rightX = d->m_curve.curvePoints()[d->m_grab_point_index + 1].x() - POINT_AREA;
489 else
490 rightX = 1.0;
491 } else if (d->m_grab_point_index == d->m_curve.curvePoints().count() - 1) {
492 leftX = d->m_curve.curvePoints()[d->m_grab_point_index - 1].x() + POINT_AREA;
493 rightX = 1.0;
494 } else {
495 Q_ASSERT(d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.curvePoints().count() - 1);
496
497 // the 1E-4 addition so we can grab the dot later.
498 leftX = d->m_curve.curvePoints()[d->m_grab_point_index - 1].x() + POINT_AREA;
499 rightX = d->m_curve.curvePoints()[d->m_grab_point_index + 1].x() - POINT_AREA;
500 }
501
502 x = bounds(x, leftX, rightX);
503 y = bounds(y, 0., 1.);
504
505 d->m_curve.setPointPosition(d->m_grab_point_index, QPointF(x, y));
506
507 if (removePoint && d->m_curve.curvePoints().count() > 2) {
508 d->m_draggedAwayPoint = d->m_curve.curvePoints()[d->m_grab_point_index];
509 d->m_draggedAwayPointIndex = d->m_grab_point_index;
510 d->m_curve.removePoint(d->m_grab_point_index);
511 d->m_grab_point_index = bounds(d->m_grab_point_index, 0, d->m_curve.curvePoints().count() - 1);
512 Q_EMIT pointSelectedChanged();
513 }
514
515 d->setCurveModified();
516 }
517}
518
520{
521 return d->m_curve;
522}
523
525{
526 d->m_curve = inlist;
527 d->m_grab_point_index = qBound(0, d->m_grab_point_index, d->m_curve.curvePoints().count() - 1);
528 d->applyGlobalPointConstrain();
529 d->setCurveModified();
530 Q_EMIT pointSelectedChanged();
531}
532
534{
535}
536
538{
539 Q_EMIT modified();
540 Q_EMIT curveChanged(d->m_curve);
541}
542
544{
545 d->m_modifiedSignalsCompressor.start();
546}
const Params2D p
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
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