Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_slider_spin_box_p.h
Go to the documentation of this file.
1/* This file is part of the KDE project
2 *
3 * SPDX-FileCopyrightText: 2010 Justin Noel <justin@ics.com>
4 * SPDX-FileCopyrightText: 2010 Cyrille Berger <cberger@cberger.net>
5 * SPDX-FileCopyrightText: 2015 Moritz Molch <kde@moritzmolch.de>
6 * SPDX-FileCopyrightText: 2021 Deif Lou <ginoba@gmail.com>
7 *
8 * SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10
11#ifndef KISSLIDERSPINBOXPRIVATE_H
12#define KISSLIDERSPINBOXPRIVATE_H
13
14#include <QObject>
15#include <QLineEdit>
16#include <QStyleOptionSpinBox>
17#include <QPainter>
18#include <QEvent>
19#include <QInputMethodQueryEvent>
20#include <QApplication>
21#include <QMouseEvent>
22#include <QTimer>
23#include <QStyleHints>
24#include <QMenu>
25#include <QVariantAnimation>
26#include <QPointer>
27
28#include <cmath>
29#include <utility>
30#include <type_traits>
31
32#include <ksharedconfig.h>
33#include <kconfiggroup.h>
34#include <kis_cursor.h>
35#include <kis_num_parser.h>
36#include <klocalizedstring.h>
37#include <kis_painting_tweaks.h>
40#include <kis_algebra_2d.h>
42
43template <typename SpinBoxTypeTP, typename BaseSpinBoxTypeTP>
44class Q_DECL_HIDDEN KisSliderSpinBoxPrivate : public QObject
45{
46public:
47 using SpinBoxType = SpinBoxTypeTP;
48 using BaseSpinBoxType = BaseSpinBoxTypeTP;
49 using ValueType = decltype(std::declval<SpinBoxType>().value());
50
52 {
55 ValueUpdateMode_UseValueBeforeEditing
56 };
57
59 : m_q(q)
60 , m_lineEdit(m_q->lineEdit())
61 , m_startEditingSignalProxy(std::bind(&KisSliderSpinBoxPrivate::startEditing, this))
62 {
63 m_q->installEventFilter(this);
64
65 m_lineEdit->setReadOnly(true);
66 m_lineEdit->setAlignment(Qt::AlignCenter);
67 m_lineEdit->setAutoFillBackground(false);
68 m_lineEdit->setCursor(KisCursor::splitHCursor());
69 m_lineEdit->installEventFilter(this);
70
71 m_widgetRangeToggle = new QWidget(m_q);
72 m_widgetRangeToggle->hide();
73 m_widgetRangeToggle->installEventFilter(this);
74
75 m_sliderAnimation.setStartValue(0.0);
76 m_sliderAnimation.setEndValue(1.0);
77 m_sliderAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InOutCubic));
78 connect(&m_sliderAnimation, &QVariantAnimation::valueChanged, m_lineEdit, QOverload<>::of(&QLineEdit::update));
79 connect(&m_sliderAnimation, &QVariantAnimation::valueChanged, m_widgetRangeToggle, QOverload<>::of(&QLineEdit::update));
80
81 m_rangeToggleHoverAnimation.setStartValue(0.0);
82 m_rangeToggleHoverAnimation.setEndValue(1.0);
83 m_rangeToggleHoverAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InOutCubic));
84 connect(&m_rangeToggleHoverAnimation, &QVariantAnimation::valueChanged, m_widgetRangeToggle, QOverload<>::of(&QLineEdit::update));
85 }
86
88 {
89 if (isEditModeActive()) {
90 return;
91 }
92 // Store the current value
93 m_valueBeforeEditing = m_q->value();
94 m_lineEdit->setReadOnly(false);
95 m_q->selectAll();
96 m_lineEdit->setFocus(Qt::OtherFocusReason);
97 m_lineEdit->setCursor(KisCursor::ibeamCursor());
98 }
99
100 void endEditing(ValueUpdateMode updateMode = ValueUpdateMode_UseLastValidValue)
101 {
102 if (!isEditModeActive()) {
103 return;
104 }
105 if (updateMode == ValueUpdateMode_UseLastValidValue) {
106 setValue(m_q->value(), false, false, true);
107 } else if (updateMode == ValueUpdateMode_UseValueBeforeEditing) {
108 setValue(m_valueBeforeEditing, false, false, true);
109 }
110 // Restore palette colors
111 QPalette pal = m_lineEdit->palette();
112 pal.setBrush(QPalette::Text, m_q->palette().text());
113 m_lineEdit->setPalette(pal);
114 m_rightClickCounter = 0;
115 m_lineEdit->setReadOnly(true);
116 m_lineEdit->setCursor(KisCursor::splitHCursor());
117 m_lineEdit->update();
118 m_q->update();
119 }
120
121 bool isEditModeActive() const
122 {
123 return !m_lineEdit->isReadOnly();
124 }
125
126 // Compute a new value as a function of the x and y coordinates relative
127 // to the lineedit, and some combination of modifiers
128 ValueType valueForPoint(const QPoint &p, Qt::KeyboardModifiers modifiers) const
129 {
130 const QRectF rect(m_lineEdit->rect());
131 const QPointF center(
132 static_cast<double>(
133 m_lastMousePressPosition.x() + (m_useRelativeDragging ? m_relativeDraggingOffset : 0)
134 ),
135 rect.height() / 2.0
136 );
137 const bool useSoftRange = isSoftRangeValid() && (m_softRangeViewMode == SoftRangeViewMode_AlwaysShowSoftRange || m_isSoftRangeActive);
138 const double minimum = static_cast<double>(useSoftRange ? m_softMinimum : m_q->minimum());
139 const double maximum = static_cast<double>(useSoftRange ? m_softMaximum : m_q->maximum());
140 const double rangeSize = maximum - minimum;
141 // Get the distance relative to the line edit center and transformed
142 // so that it starts counting 32px away from the widget. If the position
143 // is inside the widget or that 32px area the distance will be 0 so that
144 // the value change will be the same near the widget
145 const double distanceY =
146 std::max(
147 0.0,
148 std::abs(static_cast<double>(p.y()) - center.y()) - center.y() - constantDraggingMargin
149 );
150 // Get the scale
151 double scale;
152 if (modifiers & Qt::ShiftModifier) {
153 // If the shift key is pressed we scale the distanceY value to make the scale
154 // have a stronger effect and also offset it so that the minimum
155 // scale will be 5x (1x + 4x).
156 // function
157 scale = (rect.width() + 2.0 * distanceY * 10.0) / rect.width() + 4.0;
158 } else {
159 // Get the scale as a function of the vertical position
160 scale = (rect.width() + 2.0 * distanceY * 2.0) / rect.width();
161 }
162 // Scale the horizontal coordinates around where we first clicked
163 // as a function of the y coordinate
164 const double scaledRectLeft = (0.0 - center.x()) * scale + center.x();
165 const double scaledRectRight = (rect.width() - center.x()) * scale + center.x();
166 // Map the current horizontal position to the new rect
167 const double scaledRectWidth = scaledRectRight - scaledRectLeft;
168 const double posX = static_cast<double>(p.x()) - scaledRectLeft;
169 // Normalize
170 const double normalizedPosX = qBound(0.0, posX / scaledRectWidth, 1.0);
171 // Final value
172 const double normalizedValue = std::pow(normalizedPosX, m_exponentRatio);
173 double value = normalizedValue * rangeSize + minimum;
174 // If key CTRL is pressed, round to the closest step.
175 if (modifiers & Qt::ControlModifier) {
176 value = std::round(value / m_fastSliderStep) * m_fastSliderStep;
177 }
178 //Return the value
179 if (std::is_same<ValueType, double>::value) {
180 return value;
181 } else {
182 return static_cast<ValueType>(std::round(value));
183 }
184 }
185
187 {
188 ValueType min, max;
189 if (isSoftRangeValid()) {
190 if (m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges) {
191 if (m_isSoftRangeActive) {
192 min = softMinimum();
193 max = softMaximum();
194 } else {
195 min = m_q->minimum();
196 max = m_q->maximum();
197 }
198 } else {
199 min = softMinimum();
200 max = softMaximum();
201 }
202 } else {
203 min = m_q->minimum();
204 max = m_q->maximum();
205 }
206 return QPoint(static_cast<int>(qRound(computeSliderWidth(min, max, value))), 0);
207 }
208
209 // Custom setValue that allows disabling signal emission
210 void setValue(ValueType newValue,
211 bool blockSignals = false,
212 bool emitSignalsEvenWhenValueNotChanged = false,
213 bool overwriteExpression = false)
214 {
215 if (blockSignals) {
216 m_q->blockSignals(true);
217 m_q->BaseSpinBoxType::setValue(newValue, overwriteExpression);
218 m_q->blockSignals(false);
219 } else {
220 ValueType v = m_q->value();
221 m_q->BaseSpinBoxType::setValue(newValue, overwriteExpression);
222 if (v == m_q->value() && emitSignalsEvenWhenValueNotChanged) {
223 emitSignals();
224 }
225 }
226 if (!m_q->hasFocus()) {
227 endEditing(ValueUpdateMode_NoChange);
228 }
229 }
230
232 {
233 if (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges) {
234 if (m_isSoftRangeActive) {
235 makeSoftRangeActive();
236 } else {
237 makeHardRangeActive();
238 }
239 updateWidgetRangeToggleTooltip();
240 m_widgetRangeToggle->show();
241 } else {
242 m_sliderAnimation.stop();
243 m_widgetRangeToggle->hide();
244 }
245 qResizeEvent(nullptr);
246 }
247
248 template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, int>::value, U>::type>
249 void setRange(int newMinimum, int newMaximum, bool computeNewFastSliderStep)
250 {
251 m_q->BaseSpinBoxType::setRange(newMinimum, newMaximum);
252 if (computeNewFastSliderStep) {
253 // Behavior taken from the old slider spinbox. Kind of arbitrary
254 m_fastSliderStep = (m_q->maximum() - m_q->minimum()) / 20;
255 if (m_fastSliderStep == 0) {
256 m_fastSliderStep = 1;
257 }
258 }
259 m_softMinimum = qBound(m_q->minimum(), m_softMinimum, m_q->maximum());
260 m_softMaximum = qBound(m_q->minimum(), m_softMaximum, m_q->maximum());
261 resetRangeMode();
262 m_q->update();
263 }
264
265 template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, double>::value, U>::type>
266 void setRange(double newMinimum, double newMaximum, int newNumberOfDecimals, bool computeNewFastSliderStep)
267 {
268 m_q->setDecimals(newNumberOfDecimals);
269 m_q->BaseSpinBoxType::setRange(newMinimum, newMaximum);
270 if (computeNewFastSliderStep) {
271 // Behavior taken from the old slider. Kind of arbitrary
272 const double rangeSize = m_q->maximum() - m_q->minimum();
273 if (rangeSize >= 2.0 || newNumberOfDecimals <= 0) {
274 m_fastSliderStep = 1.0;
275 } else if (newNumberOfDecimals == 1) {
276 m_fastSliderStep = rangeSize / 10.0;
277 } else {
278 m_fastSliderStep = rangeSize / 20.0;
279 }
280 }
281 m_softMinimum = qBound(m_q->minimum(), m_softMinimum, m_q->maximum());
282 m_softMaximum = qBound(m_q->minimum(), m_softMaximum, m_q->maximum());
283 resetRangeMode();
284 m_lineEdit->update();
285 }
286
287 void setBlockUpdateSignalOnDrag(bool newBlockUpdateSignalOnDrag)
288 {
289 m_blockUpdateSignalOnDrag = newBlockUpdateSignalOnDrag;
290 }
291
292 void setFastSliderStep(ValueType newFastSliderStep)
293 {
294 m_fastSliderStep = newFastSliderStep;
295 }
296
297 // Set the soft range. Set newSoftMinimum = newSoftMaximum to signal that
298 // the soft range must not be used
299 void setSoftRange(ValueType newSoftMinimum, ValueType newSoftMaximum)
300 {
301 if ((newSoftMinimum != newSoftMaximum) &&
302 (newSoftMinimum > newSoftMaximum || newSoftMinimum < m_q->minimum() || newSoftMaximum > m_q->maximum())) {
303 return;
304 }
305 m_softMinimum = newSoftMinimum;
306 m_softMaximum = newSoftMaximum;
307 resetRangeMode();
308 m_lineEdit->update();
309 }
310
311 bool isSoftRangeValid() const
312 {
313 return m_softMaximum > m_softMinimum;
314 }
315
317 {
318 return m_fastSliderStep;
319 }
320
322 {
323 return m_softMinimum;
324 }
325
327 {
328 return m_softMaximum;
329 }
330
331 bool isDragging() const
332 {
333 return m_isDragging;
334 }
335
337 {
338 m_sliderAnimation.stop();
339 m_isSoftRangeActive = true;
340 // scale the animation duration in case the animation is in the middle
341 const int animationDuration =
342 static_cast<int>(std::round(m_sliderAnimation.currentValue().toReal() * fullAnimationDuration));
343 m_sliderAnimation.setStartValue(m_sliderAnimation.currentValue());
344 m_sliderAnimation.setEndValue(0.0);
345 m_sliderAnimation.setDuration(animationDuration);
346 m_sliderAnimation.start();
347 }
348
350 {
351 m_sliderAnimation.stop();
352 m_isSoftRangeActive = false;
353 // scale the duration in case the animation is in the middle
354 const int animationDuration =
355 static_cast<int>(std::round((1.0 - m_sliderAnimation.currentValue().toReal()) * fullAnimationDuration));
356 m_sliderAnimation.setStartValue(m_sliderAnimation.currentValue());
357 m_sliderAnimation.setEndValue(1.0);
358 m_sliderAnimation.setDuration(animationDuration);
359 m_sliderAnimation.start();
360 }
361
362 void setExponentRatio(double newExponentRatio)
363 {
364 m_exponentRatio = newExponentRatio;
365 m_lineEdit->update();
366 }
367
369 {
370 m_widgetRangeToggle->setToolTip(
371 i18nc(
372 "@info:tooltip toggle between soft and hard range in the slider spin box",
373 "Toggle between full range and subrange.\nFull range: [%1, %2]\nSubrange: [%3, %4]",
374 QString::number(m_q->minimum()),
375 QString::number(m_q->maximum()),
376 QString::number(m_softMinimum),
377 QString::number(m_softMaximum)
378 )
379 );
380 }
381
382 QSize sizeHint() const
383 {
384 QSize hint = m_q->BaseSpinBoxType::sizeHint();
385 return
386 (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges)
387 ? QSize(hint.width() + widthOfRangeModeToggle, hint.height())
388 : hint;
389 }
390
391 QSize minimumSizeHint() const
392 {
393 QSize hint = m_q->BaseSpinBoxType::minimumSizeHint();
394 return
395 (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges)
396 ? QSize(hint.width() + widthOfRangeModeToggle, hint.height())
397 : hint;
398 }
399
400 void emitSignals() const
401 {
402#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
403 Q_EMIT m_q->textChanged(m_q->text());
404#else
405 Q_EMIT m_q->valueChanged(m_q->text());
406#endif
407 Q_EMIT m_q->valueChanged(m_q->value());
408 }
409
410 bool qResizeEvent(QResizeEvent*)
411 {
412 // When resizing the spinbox, perform style specific positioning
413 // of the lineedit
414
415 // Get the default rect for the lineedit widget
416 QStyleOptionSpinBox spinBoxOptions;
417 m_q->initStyleOption(&spinBoxOptions);
418 QRect rect = m_q->style()->subControlRect(QStyle::CC_SpinBox, &spinBoxOptions, QStyle::SC_SpinBoxEditField);
419 // Offset the rect to make it take all the available space inside the
420 // spinbox, without overlapping the buttons
421 QString style = qApp->property(currentUnderlyingStyleNameProperty).toString().toLower();
422 if (style == "breeze") {
423 rect.adjust(-4, -4, 0, 4);
424 } else if (style == "fusion") {
425 rect.adjust(-2, -1, 2, 1);
426 }
427 // Set the rect
428 if (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges) {
429 m_lineEdit->setGeometry(rect.adjusted(0, 0, -widthOfRangeModeToggle, 0));
430 m_widgetRangeToggle->setGeometry(rect.adjusted(rect.width() - widthOfRangeModeToggle, 0, 0, 0));
431 } else {
432 m_lineEdit->setGeometry(rect);
433 }
434
435 return true;
436 }
437
438 bool qFocusOutEvent(QFocusEvent*)
439 {
440 // If the focus is lost then the edition stops, unless the focus
441 // was lost because the menu was shown
442 if (m_focusLostDueToMenu) {
443 m_focusLostDueToMenu = false;
444 } else {
445 if (m_q->isLastValid()) {
446 endEditing();
447 }
448 }
449 return false;
450 }
451
452 bool qMousePressEvent(QMouseEvent*)
453 {
454 // If we click in any part of the spinbox outside the lineedit
455 // then the edition stops
456 endEditing();
457 return false;
458 }
459
460 bool qKeyPressEvent(QKeyEvent *e)
461 {
462 switch (e->key()) {
463 // If the lineedit is not in edition mode, make the left and right
464 // keys have the same effect as the down and up keys. This replicates
465 // the behaviour of the old slider spinbox
466 case Qt::Key_Right:
467 if (!isEditModeActive()) {
468 qApp->postEvent(m_q, new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, e->modifiers()));
469 return true;
470 }
471 break;
472 case Qt::Key_Left:
473 if (!isEditModeActive()) {
474 qApp->postEvent(m_q, new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, e->modifiers()));
475 return true;
476 }
477 break;
478 // The enter key can be used to enter the edition mode if the
479 // lineedit is not in it or to commit the entered value and leave
480 // the edition mode if we are in it
481 case Qt::Key_Enter:
482 case Qt::Key_Return:
483 if (!e->isAutoRepeat()) {
484 if (!isEditModeActive()) {
485 startEditing();
486 } else {
487 if (m_q->isLastValid()) {
488 endEditing();
489 } else {
490 return false;
491 }
492 }
493 }
494 return true;
495 // The escape key can be used to leave the edition mode rejecting
496 // the written value
497 case Qt::Key_Escape:
498 if (isEditModeActive()) {
499 endEditing(ValueUpdateMode_UseValueBeforeEditing);
500 return true;
501 }
502 break;
503 // If we press a number key when in slider mode, then start the edit
504 // mode and resend the key event so that the key is processed again
505 // in edit mode. Since entering edit mode selects all text, the new
506 // event will replace the text by the new key text
507 default:
508 if (!isEditModeActive() && e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) {
509 startEditing();
510 qApp->postEvent(m_q, new QKeyEvent(QEvent::KeyPress, e->key(), e->modifiers(), e->text(), e->isAutoRepeat()));
511 return true;
512 }
513 break;
514 }
515 return false;
516 }
517
518 bool qContextMenuEvent(QContextMenuEvent *e)
519 {
520 // Shows a menu. Code inspired by the QAbstractSpinBox
521 // contextMenuEvent function
522
523 // Return if we are in slider mode and the mouse is in the line edit
524 if (!isEditModeActive() && m_lineEdit->rect().contains(e->pos())) {
525 return true;
526 }
527 // Create the menu
528 QPointer<QMenu> menu;
529 // If we are in edit mode, then add the line edit
530 // actions
531 if (isEditModeActive()) {
532 menu = m_lineEdit->createStandardContextMenu();
533 m_focusLostDueToMenu = true;
534 } else {
535 menu = new QMenu;
536 }
537 if (!menu) {
538 return true;
539 }
540 // Override select all action
541 QAction *selectAllAction = nullptr;
542 if (isEditModeActive()) {
543 selectAllAction = new QAction(i18nc("Menu action to select all text in the slider spin box", "&Select All"), menu);
544#if QT_CONFIG(shortcut)
545 selectAllAction->setShortcut(QKeySequence::SelectAll);
546#endif
547 menu->removeAction(menu->actions().last());
548 menu->addAction(selectAllAction);
549 menu->addSeparator();
550 }
551 // Add step up and step down actions
552 const uint stepEnabled = m_q->stepEnabled();
553 QAction *stepUpAction = menu->addAction(i18nc("Menu action to step up in the slider spin box", "&Step up"));
554 stepUpAction->setEnabled(stepEnabled & SpinBoxType::StepUpEnabled);
555 QAction *stepDown = menu->addAction(i18nc("Menu action to step down in the slider spin box", "Step &down"));
556 stepDown->setEnabled(stepEnabled & SpinBoxType::StepDownEnabled);
557 menu->addSeparator();
558 // This code is taken from QAbstractSpinBox. Use a QPointer in case the
559 // spin box is destroyed while the menu is shown??
560 const QPointer<SpinBoxType> spinbox = m_q;
561 const QPoint pos =
562 (e->reason() == QContextMenuEvent::Mouse)
563 ? e->globalPos()
564 : m_q->mapToGlobal(QPoint(e->pos().x(), 0)) + QPoint(m_q->width() / 2, m_q->height() / 2);
565 const QAction *action = menu->exec(pos);
566 delete static_cast<QMenu *>(menu);
567 if (spinbox && action) {
568 if (action == stepUpAction) {
569 m_q->stepBy(static_cast<ValueType>(1));
570 } else if (action == stepDown) {
571 m_q->stepBy(static_cast<ValueType>(-1));
572 } else if (action == selectAllAction) {
573 m_q->selectAll();
574 }
575 }
576 e->accept();
577 return true;
578 }
579
580 bool blockInputMethodQuery(QInputMethodQueryEvent *e)
581 {
582 // On Android, pressing on a text input shows text selection handles,
583 // see QAndroidInputContext::updateSelectionHandles. We don't want that
584 // if a slider is not in edit mode, so we respond with "not enabled".
585 if (e->queries().testFlag(Qt::ImEnabled) && !isEditModeActive()) {
586 e->setValue(Qt::ImEnabled, false);
587 e->accept();
588 return true;
589 } else {
590 return false;
591 }
592 }
593
594 // Generic "style aware" helper function to draw a rect
595 void paintSliderRect(QPainter &painter, const QRectF &rect, const QBrush &brush)
596 {
597 painter.save();
598 painter.setBrush(brush);
599 painter.setPen(Qt::NoPen);
600 if (qApp->property(currentUnderlyingStyleNameProperty).toString().toLower() == "fusion") {
601 painter.drawRoundedRect(rect, 1, 1);
602 } else {
603 painter.drawRoundedRect(rect, 0, 0);
604 }
605 painter.restore();
606 }
607
608 void paintSliderText(QPainter &painter, const QString &text, const QRectF &rect, const QRectF &clipRect, const QColor &color, const QTextOption &textOption)
609 {
610 painter.setBrush(Qt::NoBrush);
611 painter.setPen(color);
612 painter.setClipping(true);
613 painter.setClipRect(clipRect);
614 painter.drawText(rect, text, textOption);
615 painter.setClipping(false);
616 };
617
618 void paintGenericSliderText(QPainter &painter, const QString &text, const QRectF &rect, const QRectF &sliderRect)
619 {
620 QTextOption textOption(Qt::AlignAbsolute | Qt::AlignHCenter | Qt::AlignVCenter);
621 textOption.setWrapMode(QTextOption::NoWrap);
622 // Draw portion of the text that is over the background
623 paintSliderText(painter, text, rect, rect.adjusted(sliderRect.width(), 0, 0, 0), m_lineEdit->palette().text().color(), textOption);
624 // Draw portion of the text that is over the progress bar
625 paintSliderText(painter, text, rect, sliderRect, m_lineEdit->palette().highlightedText().color(), textOption);
626 };
627
628 void paintSlider(QPainter &painter, const QString &text, double slider01Width, double slider02Width = -1.0)
629 {
630 const QRectF rect = m_lineEdit->rect();
631 const QColor highlightColor = m_q->palette().highlight().color();
632 if (slider02Width < 0.0) {
633 const QRectF sliderRect = rect.adjusted(0, 0, -(rect.width() - slider01Width), 0);
634 paintSliderRect(painter, sliderRect, highlightColor);
635 if (!text.isNull()) {
636 paintGenericSliderText(painter, text, rect, sliderRect);
637 }
638 } else {
639 static constexpr double heightOfCollapsedSliderPlusSpace = heightOfCollapsedSlider + heightOfSpaceBetweenSliders;
640 const double heightBetween = rect.height() - 2.0 * heightOfCollapsedSlider - heightOfSpaceBetweenSliders;
641 const double animationPos = m_sliderAnimation.currentValue().toReal();
642 const double a = heightOfCollapsedSliderPlusSpace;
643 const double b = heightOfCollapsedSliderPlusSpace + heightBetween;
644 // Paint background text
645 QTextOption textOption(Qt::AlignAbsolute | Qt::AlignHCenter | Qt::AlignVCenter);
646 textOption.setWrapMode(QTextOption::NoWrap);
647 paintSliderText(painter, text, rect, rect, m_lineEdit->palette().text().color(), textOption);
648 // Paint soft range slider
649 const QColor softSliderColor = KisPaintingTweaks::blendColors(highlightColor, m_q->palette().base().color(), 1.0 - 0.25);
650 const double softSliderAdjustment = -KisAlgebra2D::lerp(a, b, animationPos);
651 paintSliderRect(
652 painter,
653 rect.adjusted(0, 0, -(rect.width() - slider02Width), softSliderAdjustment),
654 softSliderColor
655 );
656 // Paint hard range slider
657 const double hardSliderAdjustment = KisAlgebra2D::lerp(a, b, 1.0 - animationPos);
658 paintSliderRect(
659 painter,
660 rect.adjusted(0, hardSliderAdjustment, -(rect.width() - slider01Width), 0),
661 highlightColor
662 );
663 // Paint text in the sliders
664 paintSliderText(
665 painter, text, rect,
666 rect.adjusted(0, 0, -(rect.width() - slider02Width), softSliderAdjustment),
667 m_lineEdit->palette().highlightedText().color(),
668 textOption
669 );
670 paintSliderText(
671 painter, text, rect,
672 rect.adjusted(0, hardSliderAdjustment, -(rect.width() - slider01Width), 0),
673 m_lineEdit->palette().highlightedText().color(),
674 textOption
675 );
676 }
677 }
678
679 double computeSliderWidth(double min, double max, double value) const
680 {
681 const double rangeSize = max - min;
682 const double localPosition = value - min;
683 const double normalizedValue = std::pow(localPosition / rangeSize, 1.0 / m_exponentRatio);
684 const double width = static_cast<double>(m_lineEdit->width());
685 return qBound(0.0, std::round(normalizedValue * width), width);
686 }
687
688 bool lineEditPaintEvent(QPaintEvent*)
689 {
690 QPainter painter(m_lineEdit);
691 painter.setRenderHint(QPainter::Antialiasing, true);
692
693 const double value = m_q->value();
694
695 // If we are not editing, just draw the text, otherwise draw a
696 // semi-transparent rect to dim the background and let the QLineEdit
697 // draw the rest (text, selection, cursor, etc.)
698 const double hardSliderWidth = computeSliderWidth(static_cast<double>(m_q->minimum()), static_cast<double>(m_q->maximum()), value);
699 const double softSliderWidth = computeSliderWidth(m_softMinimum, m_softMaximum, value);
700 if (!isEditModeActive()) {
701 QString text = m_q->text();
702 if (isSoftRangeValid()) {
703 if (m_softRangeViewMode == SoftRangeViewMode_AlwaysShowSoftRange) {
704 paintSlider(painter, text, softSliderWidth);
705 } else {
706 paintSlider(painter, text, hardSliderWidth, softSliderWidth);
707 }
708 } else {
709 // Draw the slider
710 paintSlider(painter, text, hardSliderWidth);
711 }
712 } else {
713 // Draw the slider
714 if (isSoftRangeValid()) {
715 if (m_softRangeViewMode == SoftRangeViewMode_AlwaysShowSoftRange) {
716 paintSlider(painter, QString(), softSliderWidth);
717 } else {
718 paintSlider(painter, QString(), hardSliderWidth, softSliderWidth);
719 }
720 } else {
721 paintSlider(painter, QString(), hardSliderWidth);
722 }
723 // Paint the overlay with the base color
724 QColor color = m_q->palette().base().color();
725 color.setAlpha(128);
726 paintSliderRect(painter, m_lineEdit->rect(), color);
727 }
728 // If we are editing the text then return false and let the QLineEdit
729 // paint all the edit related stuff (e.g. selection)
730 return !isEditModeActive();
731 }
732
733 bool lineEditMousePressEvent(QMouseEvent *e, bool edit)
734 {
735 if (!m_q->isEnabled()) {
736 return false;
737 }
738 if (!isEditModeActive()) {
739 if (e->button() == Qt::LeftButton) {
740 m_lastMousePressPosition = e->pos();
741 const QPoint currentValuePosition = pointForValue(m_q->value());
742 m_relativeDraggingOffset = currentValuePosition.x() - e->x();
743 m_useRelativeDragging = (e->modifiers() & Qt::ShiftModifier);
744 if (edit) {
745 QTimer::singleShot(0, &m_startEditingSignalProxy, SLOT(start()));
746 } else {
747 lineEditMouseMoveEvent(e);
748 }
749 }
750 return true;
751 }
752 return false;
753 }
754
755 bool lineEditMouseReleaseEvent(QMouseEvent *e)
756 {
757 if (!m_q->isEnabled()) {
758 return false;
759 }
760 if (!isEditModeActive()) {
761 // Releasing the right mouse button makes the lineedit enter
762 // the edition mode if we are not editing
763 if (e->button() == Qt::RightButton) {
764 // If we call startEditing() right from the eventFilter(),
765 // then the mouse release event will be somehow be passed
766 // to Qt further and generate ContextEvent on Windows.
767 // Therefore we should call it from a normal timer event.
768 QTimer::singleShot(0, &m_startEditingSignalProxy, SLOT(start()));
769 // Releasing the left mouse button stops the dragging. If signals
770 // must be blocked when dragging then we set the value here and
771 // Q_EMIT a signal
772 } else if (e->button() == Qt::LeftButton) {
773 if (m_blockUpdateSignalOnDrag) {
774 const QPoint p(m_useRelativeDragging ? e->pos().x() + m_relativeDraggingOffset : e->pos().x(),
775 e->pos().y());
776 setValue(valueForPoint(p, e->modifiers()), false, true);
777 } else {
778 if (!m_isDragging) {
779 setValue(valueForPoint(e->pos(), e->modifiers()), false, true);
780 }
781 }
782
783 m_isDragging = false;
784 Q_EMIT m_q->draggingFinished();
785 }
786 return true;
787 }
788 return false;
789 }
790
791 bool lineEditMouseMoveEvent(QMouseEvent *e)
792 {
793 if (!m_q->isEnabled()) {
794 return false;
795 }
796 if (!isEditModeActive()) {
797 if (e->buttons() & Qt::LeftButton) {
798 m_isDragging = true;
799 // At this point we are dragging so record the position and set
800 // the value
801 const QPoint p(m_useRelativeDragging ? e->pos().x() + m_relativeDraggingOffset : e->pos().x(),
802 e->pos().y());
803 setValue(valueForPoint(p, e->modifiers()), m_blockUpdateSignalOnDrag);
804 return true;
805 }
806 }
807 return false;
808 }
809
811 {
812 QPainter painter(m_widgetRangeToggle);
813 painter.setRenderHint(QPainter::Antialiasing, true);
814 // Compute sizes and positions
815 const double width = static_cast<double>(m_widgetRangeToggle->width());
816 const double height = static_cast<double>(m_widgetRangeToggle->height());
817 constexpr double marginX = 4.0;
818 const double toggleWidth = width - 2.0 * marginX;
819 const double centerX = width * 0.5;
820 const double centerY = height * 0.5;
821 const double bigRadius = centerX - std::floor(centerX - (toggleWidth * 0.5)) + 0.5;
822 const double smallRadius = bigRadius * 0.5;
823 const double sliderAnimationPos = m_sliderAnimation.currentValue().toReal();
824 const double radius = smallRadius + sliderAnimationPos * (bigRadius - smallRadius);
825 // Compute color
826 const double rangeToggleHoverAnimationPos = m_rangeToggleHoverAnimation.currentValue().toReal();
827 const QColor baseColor = m_q->palette().base().color();
828 const QColor textColor = m_q->palette().text().color();
829 const QColor color = KisPaintingTweaks::blendColors(baseColor, textColor, 1.0 - (0.60 + 0.40 * rangeToggleHoverAnimationPos));
830 // Paint outer circle
831 painter.setPen(color);
832 painter.setBrush(Qt::NoBrush);
833 painter.drawEllipse(QPointF(centerX, centerY), bigRadius, bigRadius);
834 // Paint dot
835 painter.setPen(Qt::NoPen);
836 painter.setBrush(color);
837 painter.drawEllipse(QPointF(centerX, centerY), radius, radius);
838 return true;
839 }
840
842 {
843 if (!m_q->isEnabled()) {
844 return false;
845 }
846 if (e->button() == Qt::LeftButton) {
847 if (!m_isSoftRangeActive) {
848 makeSoftRangeActive();
849 } else {
850 makeHardRangeActive();
851 }
852 return true;
853 }
854 return false;
855 }
856
858 {
859 m_rangeToggleHoverAnimation.stop();
860 // scale the animation duration in case the animation is in the middle
861 const int animationDuration =
862 static_cast<int>(std::round(m_rangeToggleHoverAnimation.currentValue().toReal() * fullAnimationDuration));
863 m_rangeToggleHoverAnimation.setStartValue(m_rangeToggleHoverAnimation.currentValue());
864 m_rangeToggleHoverAnimation.setEndValue(1.0);
865 m_rangeToggleHoverAnimation.setDuration(animationDuration);
866 m_rangeToggleHoverAnimation.start();
867 return false;
868 }
869
871 {
872 m_rangeToggleHoverAnimation.stop();
873 // scale the animation duration in case the animation is in the middle
874 const int animationDuration =
875 static_cast<int>(std::round(m_rangeToggleHoverAnimation.currentValue().toReal() * fullAnimationDuration));
876 m_rangeToggleHoverAnimation.setStartValue(m_rangeToggleHoverAnimation.currentValue());
877 m_rangeToggleHoverAnimation.setEndValue(0.0);
878 m_rangeToggleHoverAnimation.setDuration(animationDuration);
879 m_rangeToggleHoverAnimation.start();
880 return false;
881 }
882
883 bool eventFilter(QObject * o, QEvent * e) override
884 {
885 if (!o || !e) {
886 return false;
887 }
888 if (o == m_q) {
889 switch (e->type()) {
890 case QEvent::Resize : return qResizeEvent(static_cast<QResizeEvent*>(e));
891 case QEvent::FocusOut : return qFocusOutEvent(static_cast<QFocusEvent*>(e));
892 case QEvent::MouseButtonPress : return qMousePressEvent(static_cast<QMouseEvent*>(e));
893 case QEvent::KeyPress : return qKeyPressEvent(static_cast<QKeyEvent*>(e));
894 case QEvent::ContextMenu : return qContextMenuEvent(static_cast<QContextMenuEvent*>(e));
895 case QEvent::InputMethodQuery: return blockInputMethodQuery(static_cast<QInputMethodQueryEvent *>(e));
896 default: break;
897 }
898 } else if (o == m_lineEdit) {
899 switch (e->type()) {
900 case QEvent::Paint : return lineEditPaintEvent(static_cast<QPaintEvent*>(e));
901 case QEvent::MouseButtonPress : return lineEditMousePressEvent(static_cast<QMouseEvent*>(e), false);
902 case QEvent::MouseButtonDblClick : return lineEditMousePressEvent(static_cast<QMouseEvent*>(e), true);
903 case QEvent::MouseButtonRelease : return lineEditMouseReleaseEvent(static_cast<QMouseEvent*>(e));
904 case QEvent::MouseMove : return lineEditMouseMoveEvent(static_cast<QMouseEvent*>(e));
905 default: break;
906 }
907 } else if (o == m_widgetRangeToggle) {
908 switch (e->type()) {
909 case QEvent::Paint : return widgetRangeTogglePaintEvent(static_cast<QPaintEvent*>(e));
910 case QEvent::MouseButtonRelease: return widgetRangeToggleMouseReleaseEvent(static_cast<QMouseEvent*>(e));
911 case QEvent::Enter: return widgetRangeToggleEnterEvent(e);
912 case QEvent::Leave: return widgetRangeToggleLeaveEvent(e);
913 case QEvent::InputMethodQuery: return blockInputMethodQuery(static_cast<QInputMethodQueryEvent *>(e));
914 default: break;
915 }
916 }
917 return false;
918 }
919
920private:
921 // Margin around the spinbox for which the dragging gives same results,
922 // regardless of the vertical distance
923 static constexpr double constantDraggingMargin{32.0};
924 // Height of the collapsed slider bar
925 static constexpr double heightOfCollapsedSlider{3.0};
926 // Height of the space between the soft and hard range sliders
927 static constexpr double heightOfSpaceBetweenSliders{0.0};
928 // Width of the area to activate soft/hard range
929 static constexpr double widthOfRangeModeToggle{16.0};
930 // The duration of the animation
931 static constexpr double fullAnimationDuration{200.0};
932
933 SpinBoxType *m_q {nullptr};
934 QLineEdit *m_lineEdit {nullptr};
935 QWidget *m_widgetRangeToggle {nullptr};
936 ValueType m_softMinimum {static_cast<ValueType>(0)};
937 ValueType m_softMaximum {static_cast<ValueType>(0)};
938 double m_exponentRatio {1.0};
939 bool m_blockUpdateSignalOnDrag {false};
940 ValueType m_fastSliderStep {static_cast<ValueType>(5)};
941 mutable ValueType m_valueBeforeEditing {static_cast<ValueType>(0)};
942 bool m_isDragging {false};
943 bool m_useRelativeDragging {false};
944 int m_relativeDraggingOffset {0};
946 int m_rightClickCounter {0};
947 bool m_focusLostDueToMenu {false};
948 bool m_isSoftRangeActive {true};
949 QVariantAnimation m_sliderAnimation;
952
954 {
956 SoftRangeViewMode_ShowBothRanges
957 } m_softRangeViewMode{SoftRangeViewMode_ShowBothRanges};
958};
959
960#endif // KISSLIDERSPINBOXPRIVATE_H
float value(const T *src, size_t ch)
const Params2D p
qreal v
unsigned int uint
static QCursor ibeamCursor()
Definition kis_cursor.cc:59
static QCursor splitHCursor()
Definition kis_cursor.cc:99
void setRange(int newMinimum, int newMaximum, bool computeNewFastSliderStep)
bool lineEditMousePressEvent(QMouseEvent *e, bool edit)
void paintSliderText(QPainter &painter, const QString &text, const QRectF &rect, const QRectF &clipRect, const QColor &color, const QTextOption &textOption)
QVariantAnimation m_sliderAnimation
QVariantAnimation m_rangeToggleHoverAnimation
bool lineEditMouseMoveEvent(QMouseEvent *e)
void paintGenericSliderText(QPainter &painter, const QString &text, const QRectF &rect, const QRectF &sliderRect)
bool qMousePressEvent(QMouseEvent *)
QPoint pointForValue(ValueType value) const
bool eventFilter(QObject *o, QEvent *e) override
bool widgetRangeTogglePaintEvent(QPaintEvent *)
decltype(std::declval< SpinBoxType >().value()) ValueType
void setValue(ValueType newValue, bool blockSignals=false, bool emitSignalsEvenWhenValueNotChanged=false, bool overwriteExpression=false)
bool qContextMenuEvent(QContextMenuEvent *e)
bool widgetRangeToggleMouseReleaseEvent(QMouseEvent *e)
bool widgetRangeToggleLeaveEvent(QEvent *)
BaseSpinBoxTypeTP BaseSpinBoxType
void setSoftRange(ValueType newSoftMinimum, ValueType newSoftMaximum)
bool blockInputMethodQuery(QInputMethodQueryEvent *e)
ValueType valueForPoint(const QPoint &p, Qt::KeyboardModifiers modifiers) const
bool qFocusOutEvent(QFocusEvent *)
bool lineEditMouseReleaseEvent(QMouseEvent *e)
KisSliderSpinBoxPrivate(SpinBoxType *q)
void endEditing(ValueUpdateMode updateMode=ValueUpdateMode_UseLastValidValue)
bool lineEditPaintEvent(QPaintEvent *)
void setFastSliderStep(ValueType newFastSliderStep)
bool qResizeEvent(QResizeEvent *)
double computeSliderWidth(double min, double max, double value) const
void paintSlider(QPainter &painter, const QString &text, double slider01Width, double slider02Width=-1.0)
void setExponentRatio(double newExponentRatio)
void paintSliderRect(QPainter &painter, const QRectF &rect, const QBrush &brush)
bool qKeyPressEvent(QKeyEvent *e)
bool widgetRangeToggleEnterEvent(QEvent *)
void setRange(double newMinimum, double newMaximum, int newNumberOfDecimals, bool computeNewFastSliderStep)
SignalToFunctionProxy m_startEditingSignalProxy
void setBlockUpdateSignalOnDrag(bool newBlockUpdateSignalOnDrag)
constexpr const char * currentUnderlyingStyleNameProperty
Definition kis_global.h:116
Point lerp(const Point &pt1, const Point &pt2, qreal t)
QColor blendColors(const QColor &c1, const QColor &c2, qreal r1)