File indexing completed on 2024-05-12 16:02:28

0001 /* This file is part of the KDE project
0002  *
0003  * SPDX-FileCopyrightText: 2010 Justin Noel <justin@ics.com>
0004  * SPDX-FileCopyrightText: 2010 Cyrille Berger <cberger@cberger.net>
0005  * SPDX-FileCopyrightText: 2015 Moritz Molch <kde@moritzmolch.de>
0006  * SPDX-FileCopyrightText: 2021 Deif Lou <ginoba@gmail.com>
0007  *
0008  * SPDX-License-Identifier: LGPL-2.0-or-later
0009  */
0010 
0011 #ifndef KISSLIDERSPINBOXPRIVATE_H
0012 #define KISSLIDERSPINBOXPRIVATE_H
0013 
0014 #include <QObject>
0015 #include <QLineEdit>
0016 #include <QStyleOptionSpinBox>
0017 #include <QPainter>
0018 #include <QEvent>
0019 #include <QApplication>
0020 #include <QMouseEvent>
0021 #include <QTimer>
0022 #include <QStyleHints>
0023 #include <QMenu>
0024 #include <QVariantAnimation>
0025 #include <QPointer>
0026 
0027 #include <cmath>
0028 #include <utility>
0029 #include <type_traits>
0030 
0031 #include <ksharedconfig.h>
0032 #include <kconfiggroup.h>
0033 #include <kis_cursor.h>
0034 #include <kis_num_parser.h>
0035 #include <klocalizedstring.h>
0036 #include <kis_painting_tweaks.h>
0037 #include <kis_int_parse_spin_box.h>
0038 #include <kis_double_parse_spin_box.h>
0039 #include <kis_algebra_2d.h>
0040 #include <kis_signal_compressor_with_param.h>
0041 
0042 template <typename SpinBoxTypeTP, typename BaseSpinBoxTypeTP>
0043 class Q_DECL_HIDDEN KisSliderSpinBoxPrivate : public QObject
0044 {
0045 public:
0046     using SpinBoxType = SpinBoxTypeTP;
0047     using BaseSpinBoxType = BaseSpinBoxTypeTP;
0048     using ValueType = decltype(std::declval<SpinBoxType>().value());
0049 
0050     enum ValueUpdateMode
0051     {
0052         ValueUpdateMode_NoChange,
0053         ValueUpdateMode_UseLastValidValue,
0054         ValueUpdateMode_UseValueBeforeEditing
0055     };
0056 
0057     KisSliderSpinBoxPrivate(SpinBoxType *q)
0058         : m_q(q)
0059         , m_lineEdit(m_q->lineEdit())
0060         , m_startEditingSignalProxy(std::bind(&KisSliderSpinBoxPrivate::startEditing, this))
0061     {
0062         m_q->installEventFilter(this);
0063 
0064         m_lineEdit->setReadOnly(true);
0065         m_lineEdit->setAlignment(Qt::AlignCenter);
0066         m_lineEdit->setAutoFillBackground(false);
0067         m_lineEdit->setCursor(KisCursor::splitHCursor());
0068         m_lineEdit->installEventFilter(this);
0069 
0070         m_widgetRangeToggle = new QWidget(m_q);
0071         m_widgetRangeToggle->hide();
0072         m_widgetRangeToggle->installEventFilter(this);
0073 
0074         m_timerStartEditing.setSingleShot(true);
0075         connect(&m_timerStartEditing, &QTimer::timeout, this, &KisSliderSpinBoxPrivate::startEditing);
0076 
0077         m_sliderAnimation.setStartValue(0.0);
0078         m_sliderAnimation.setEndValue(1.0);
0079         m_sliderAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InOutCubic));
0080         connect(&m_sliderAnimation, &QVariantAnimation::valueChanged, m_lineEdit, QOverload<>::of(&QLineEdit::update));
0081         connect(&m_sliderAnimation, &QVariantAnimation::valueChanged, m_widgetRangeToggle, QOverload<>::of(&QLineEdit::update));
0082 
0083         m_rangeToggleHoverAnimation.setStartValue(0.0);
0084         m_rangeToggleHoverAnimation.setEndValue(1.0);
0085         m_rangeToggleHoverAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InOutCubic));
0086         connect(&m_rangeToggleHoverAnimation, &QVariantAnimation::valueChanged, m_widgetRangeToggle, QOverload<>::of(&QLineEdit::update));
0087     }
0088 
0089     void startEditing()
0090     {
0091         if (isEditModeActive()) {
0092             return;
0093         }
0094         // Store the current value
0095         m_valueBeforeEditing = m_q->value();
0096         m_lineEdit->setReadOnly(false);
0097         m_q->selectAll();
0098         m_lineEdit->setFocus(Qt::OtherFocusReason);
0099         m_lineEdit->setCursor(KisCursor::ibeamCursor());
0100     }
0101 
0102     void endEditing(ValueUpdateMode updateMode = ValueUpdateMode_UseLastValidValue)
0103     {
0104         if (!isEditModeActive()) {
0105             return;
0106         }
0107         if (updateMode == ValueUpdateMode_UseLastValidValue) {
0108             setValue(m_q->value(), false, false, true);
0109         } else if (updateMode == ValueUpdateMode_UseValueBeforeEditing) {
0110             setValue(m_valueBeforeEditing, false, false, true);
0111         }
0112         // Restore palette colors
0113         QPalette pal = m_lineEdit->palette();
0114         pal.setBrush(QPalette::Text, m_q->palette().text());
0115         m_lineEdit->setPalette(pal);
0116         m_rightClickCounter = 0;
0117         m_lineEdit->setReadOnly(true);
0118         m_lineEdit->setCursor(KisCursor::splitHCursor());
0119         m_lineEdit->update();
0120         m_q->update();
0121     }
0122     
0123     bool isEditModeActive() const
0124     {
0125         return !m_lineEdit->isReadOnly();
0126     }
0127 
0128     // Compute a new value as a function of the x and y coordinates relative
0129     // to the lineedit, and some combination of modifiers
0130     ValueType valueForPoint(const QPoint &p, Qt::KeyboardModifiers modifiers) const
0131     {
0132         const QRectF rect(m_lineEdit->rect());
0133         const QPointF center(
0134             static_cast<double>(
0135                 m_lastMousePressPosition.x() + (m_useRelativeDragging ? m_relativeDraggingOffset : 0)
0136              ),
0137             rect.height() / 2.0
0138         );
0139         const bool useSoftRange = isSoftRangeValid() && (m_softRangeViewMode == SoftRangeViewMode_AlwaysShowSoftRange || m_isSoftRangeActive);
0140         const double minimum = static_cast<double>(useSoftRange ? m_softMinimum : m_q->minimum());
0141         const double maximum = static_cast<double>(useSoftRange ? m_softMaximum : m_q->maximum());
0142         const double rangeSize = maximum - minimum;
0143         // Get the distance relative to the line edit center and transformed
0144         // so that it starts counting 32px away from the widget. If the position
0145         // is inside the widget or that 32px area the distance will be 0 so that
0146         // the value change will be the same near the widget
0147         const double distanceY =
0148             std::max(
0149                 0.0,
0150                 std::abs(static_cast<double>(p.y()) - center.y()) - center.y() - constantDraggingMargin
0151             );
0152         // Get the scale
0153         double scale;
0154         if (modifiers & Qt::ShiftModifier) {
0155             // If the shift key is pressed we scale the distanceY value to make the scale
0156             // have a stronger effect and also offset it so that the minimum
0157             // scale will be 5x (1x + 4x).
0158             // function
0159             scale = (rect.width() + 2.0 * distanceY * 10.0) / rect.width() + 4.0;
0160         } else {
0161             // Get the scale as a function of the vertical position
0162             scale = (rect.width() + 2.0 * distanceY * 2.0) / rect.width();
0163         }
0164         // Scale the horizontal coordinates around where we first clicked 
0165         // as a function of the y coordinate
0166         const double scaledRectLeft = (0.0 - center.x()) * scale + center.x();
0167         const double scaledRectRight = (rect.width() - center.x()) * scale + center.x();
0168         // Map the current horizontal position to the new rect
0169         const double scaledRectWidth = scaledRectRight - scaledRectLeft;
0170         const double posX = static_cast<double>(p.x()) - scaledRectLeft;
0171         // Normalize
0172         const double normalizedPosX = qBound(0.0, posX / scaledRectWidth, 1.0);
0173         // Final value
0174         const double normalizedValue = std::pow(normalizedPosX, m_exponentRatio);
0175         double value = normalizedValue * rangeSize + minimum;
0176         // If key CTRL is pressed, round to the closest step.
0177         if (modifiers & Qt::ControlModifier) {
0178             value = std::round(value / m_fastSliderStep) * m_fastSliderStep;
0179         }
0180         //Return the value
0181         if (std::is_same<ValueType, double>::value) {
0182             return value;
0183         } else {
0184             return static_cast<ValueType>(std::round(value));
0185         }
0186     }
0187 
0188     QPoint pointForValue(ValueType value) const
0189     {
0190         ValueType min, max;
0191         if (isSoftRangeValid()) {
0192             if (m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges) {
0193                 if (m_isSoftRangeActive) {
0194                     min = softMinimum();
0195                     max = softMaximum();
0196                 } else {
0197                     min = m_q->minimum();
0198                     max = m_q->maximum();
0199                 }
0200             } else {
0201                 min = softMinimum();
0202                 max = softMaximum();
0203             }
0204         } else {
0205             min = m_q->minimum();
0206             max = m_q->maximum();
0207         }
0208         return QPoint(static_cast<int>(qRound(computeSliderWidth(min, max, value))), 0);
0209     }
0210 
0211     // Custom setValue that allows disabling signal emission
0212     void setValue(ValueType newValue,
0213                   bool blockSignals = false,
0214                   bool emitSignalsEvenWhenValueNotChanged = false,
0215                   bool overwriteExpression = false)
0216     {
0217         if (blockSignals) {
0218             m_q->blockSignals(true);
0219             m_q->BaseSpinBoxType::setValue(newValue, overwriteExpression);
0220             m_q->blockSignals(false);
0221         } else {
0222             ValueType v = m_q->value();
0223             m_q->BaseSpinBoxType::setValue(newValue, overwriteExpression);
0224             if (v == m_q->value() && emitSignalsEvenWhenValueNotChanged) {
0225                 emitSignals();
0226             }
0227         }
0228         if (!m_q->hasFocus()) {
0229             endEditing(ValueUpdateMode_NoChange);
0230         }
0231     }
0232 
0233     void resetRangeMode()
0234     {
0235         if (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges) {
0236             if (m_isSoftRangeActive) {
0237                 makeSoftRangeActive();
0238             } else {
0239                 makeHardRangeActive();
0240             }
0241             updateWidgetRangeToggleTooltip();
0242             m_widgetRangeToggle->show();
0243         } else {
0244             m_sliderAnimation.stop();
0245             m_widgetRangeToggle->hide();
0246         }
0247         qResizeEvent(nullptr);
0248     }
0249 
0250     template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, int>::value, U>::type>
0251     void setRange(int newMinimum, int newMaximum, bool computeNewFastSliderStep)
0252     {
0253         m_q->BaseSpinBoxType::setRange(newMinimum, newMaximum);
0254         if (computeNewFastSliderStep) {
0255             // Behavior taken from the old slider spinbox. Kind of arbitrary
0256             m_fastSliderStep = (m_q->maximum() - m_q->minimum()) / 20;
0257             if (m_fastSliderStep == 0) {
0258                 m_fastSliderStep = 1;
0259             }
0260         }
0261         m_softMinimum = qBound(m_q->minimum(), m_softMinimum, m_q->maximum());
0262         m_softMaximum = qBound(m_q->minimum(), m_softMaximum, m_q->maximum());
0263         resetRangeMode();
0264         m_q->update();
0265     }
0266 
0267     template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, double>::value, U>::type>
0268     void setRange(double newMinimum, double newMaximum, int newNumberOfecimals, bool computeNewFastSliderStep)
0269     {
0270         m_q->setDecimals(newNumberOfecimals);
0271         m_q->BaseSpinBoxType::setRange(newMinimum, newMaximum);
0272         if (computeNewFastSliderStep) {
0273             // Behavior takem from the old slider. Kind of arbitrary
0274             const double rangeSize = m_q->maximum() - m_q->minimum();
0275             if (rangeSize >= 2.0 || newNumberOfecimals <= 0) {
0276                 m_fastSliderStep = 1.0;
0277             } else if (newNumberOfecimals == 1) {
0278                 m_fastSliderStep = rangeSize / 10.0;
0279             } else {
0280                 m_fastSliderStep = rangeSize / 20.0;
0281             }
0282         }
0283         m_softMinimum = qBound(m_q->minimum(), m_softMinimum, m_q->maximum());
0284         m_softMaximum = qBound(m_q->minimum(), m_softMaximum, m_q->maximum());
0285         resetRangeMode();
0286         m_lineEdit->update();
0287     }
0288 
0289     void setBlockUpdateSignalOnDrag(bool newBlockUpdateSignalOnDrag)
0290     {
0291         m_blockUpdateSignalOnDrag = newBlockUpdateSignalOnDrag;
0292     }
0293 
0294     void setFastSliderStep(int newFastSliderStep)
0295     {
0296         m_fastSliderStep = newFastSliderStep;
0297     }
0298 
0299     // Set the soft range. Set newSoftMinimum = newSoftMaximum to signal that
0300     // the soft range must not be used
0301     void setSoftRange(ValueType newSoftMinimum, ValueType newSoftMaximum)
0302     {
0303         if ((newSoftMinimum != newSoftMaximum) &&
0304             (newSoftMinimum > newSoftMaximum || newSoftMinimum < m_q->minimum() || newSoftMaximum > m_q->maximum())) {
0305             return;
0306         }
0307         m_softMinimum = newSoftMinimum;
0308         m_softMaximum = newSoftMaximum;
0309         resetRangeMode();
0310         m_lineEdit->update();
0311     }
0312 
0313     bool isSoftRangeValid() const
0314     {
0315         return m_softMaximum > m_softMinimum;
0316     }
0317 
0318     ValueType fastSliderStep() const
0319     {
0320         return m_fastSliderStep;
0321     }
0322 
0323     ValueType softMinimum() const
0324     {
0325         return m_softMinimum;
0326     }
0327 
0328     ValueType softMaximum() const
0329     {
0330         return m_softMaximum;
0331     }
0332 
0333     bool isDragging() const
0334     {
0335         return m_isDragging;
0336     }
0337     
0338     void makeSoftRangeActive()
0339     {
0340         m_sliderAnimation.stop();
0341         m_isSoftRangeActive = true;
0342         // scale the animation duration in case the animation is in the middle
0343         const int animationDuration =
0344             static_cast<int>(std::round(m_sliderAnimation.currentValue().toReal() * fullAnimationDuration));
0345         m_sliderAnimation.setStartValue(m_sliderAnimation.currentValue());
0346         m_sliderAnimation.setEndValue(0.0);
0347         m_sliderAnimation.setDuration(animationDuration);
0348         m_sliderAnimation.start();
0349     }
0350 
0351     void makeHardRangeActive()
0352     {
0353         m_sliderAnimation.stop();
0354         m_isSoftRangeActive = false;
0355         // scale the duration in case the animation is in the middle
0356         const int animationDuration =
0357             static_cast<int>(std::round((1.0 - m_sliderAnimation.currentValue().toReal()) * fullAnimationDuration));
0358         m_sliderAnimation.setStartValue(m_sliderAnimation.currentValue());
0359         m_sliderAnimation.setEndValue(1.0);
0360         m_sliderAnimation.setDuration(animationDuration);
0361         m_sliderAnimation.start();
0362     }
0363 
0364     void setExponentRatio(double newExponentRatio)
0365     {
0366         m_exponentRatio = newExponentRatio;
0367         m_lineEdit->update();
0368     }
0369 
0370     void updateWidgetRangeToggleTooltip()
0371     {
0372         m_widgetRangeToggle->setToolTip(
0373             i18nc(
0374                 "@info:tooltip toggle between soft and hard range in the slider spin box",
0375                 "Toggle between full range and subrange.\nFull range: [%1, %2]\nSubrange: [%3, %4]",
0376                 QString::number(m_q->minimum()),
0377                 QString::number(m_q->maximum()),
0378                 QString::number(m_softMinimum),
0379                 QString::number(m_softMaximum)
0380             )
0381         );
0382     }
0383 
0384     QSize sizeHint() const
0385     {
0386         QSize hint = m_q->BaseSpinBoxType::sizeHint();
0387         return
0388             (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges)
0389             ? QSize(hint.width() + widthOfRangeModeToggle, hint.height())
0390             : hint;
0391     }
0392 
0393     QSize minimumSizeHint() const
0394     {
0395         QSize hint = m_q->BaseSpinBoxType::minimumSizeHint();
0396         return
0397             (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges)
0398             ? QSize(hint.width() + widthOfRangeModeToggle, hint.height())
0399             : hint;
0400     }
0401 
0402     void emitSignals() const
0403     {
0404 #if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
0405         emit m_q->textChanged(m_q->text());
0406 #else
0407         emit m_q->valueChanged(m_q->text());
0408 #endif
0409         emit m_q->valueChanged(m_q->value());
0410     }
0411 
0412     bool qResizeEvent(QResizeEvent*)
0413     {
0414         // When resizing the spinbox, perform style specific positioning
0415         // of the lineedit
0416         
0417         // Get the default rect for the lineedit widget
0418         QStyleOptionSpinBox spinBoxOptions;
0419         m_q->initStyleOption(&spinBoxOptions);
0420         QRect rect = m_q->style()->subControlRect(QStyle::CC_SpinBox, &spinBoxOptions, QStyle::SC_SpinBoxEditField);
0421         // Offset the rect to make it take all the available space inside the
0422         // spinbox, without overlapping the buttons
0423         QString style = qApp->property(currentUnderlyingStyleNameProperty).toString().toLower();
0424         if (style == "breeze") {
0425             rect.adjust(-4, -4, 0, 4);
0426         } else if (style == "fusion") {
0427             rect.adjust(-2, -1, 2, 1);
0428         }
0429         // Set the rect
0430         if (isSoftRangeValid() && m_softRangeViewMode == SoftRangeViewMode_ShowBothRanges) {
0431             m_lineEdit->setGeometry(rect.adjusted(0, 0, -widthOfRangeModeToggle, 0));
0432             m_widgetRangeToggle->setGeometry(rect.adjusted(rect.width() - widthOfRangeModeToggle, 0, 0, 0));
0433         } else {
0434             m_lineEdit->setGeometry(rect);
0435         }
0436 
0437         return true;
0438     }
0439 
0440     bool qFocusOutEvent(QFocusEvent*)
0441     {
0442         // If the focus is lost then the edition stops, unless the focus
0443         // was lost because the menu was shown
0444         if (m_focusLostDueToMenu) {
0445             m_focusLostDueToMenu = false;
0446         } else {
0447             if (m_q->isLastValid()) {
0448                 endEditing();
0449             }
0450         }
0451         return false;
0452     }
0453 
0454     bool qMousePressEvent(QMouseEvent*)
0455     {
0456         // If we click in any part of the spinbox outside the lineedit
0457         // then the edition stops
0458         endEditing();
0459         return false;
0460     }
0461 
0462     bool qKeyPressEvent(QKeyEvent *e)
0463     {
0464         switch (e->key()) {
0465             // If the lineedit is not in edition mode, make the left and right
0466             // keys have the same effect as the down and up keys. This replicates
0467             // the behaviour of the old slider spinbox
0468             case Qt::Key_Right:
0469                 if (!isEditModeActive()) {
0470                     qApp->postEvent(m_q, new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, e->modifiers()));
0471                     return true;
0472                 }
0473                 break;
0474             case Qt::Key_Left:
0475                 if (!isEditModeActive()) {
0476                     qApp->postEvent(m_q, new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, e->modifiers()));
0477                     return true;
0478                 }
0479                 break;
0480             // The enter key can be used to enter the edition mode if the
0481             // lineedit is not in it or to commit the entered value and leave
0482             // the edition mode if we are in it
0483             case Qt::Key_Enter:
0484             case Qt::Key_Return:
0485                 if (!e->isAutoRepeat()) {
0486                     if (!isEditModeActive()) {
0487                         startEditing();
0488                     } else {
0489                         if (m_q->isLastValid()) {
0490                             endEditing();
0491                         } else {
0492                             return false;
0493                         }
0494                     }
0495                 }
0496                 return true;
0497             // The escape key can be used to leave the edition mode rejecting
0498             // the written value 
0499             case Qt::Key_Escape:
0500                 if (isEditModeActive()) {
0501                     endEditing(ValueUpdateMode_UseValueBeforeEditing);
0502                     return true;
0503                 }
0504                 break;
0505             // If we press a number key when in slider mode, then start the edit
0506             // mode and resend the key event so that the key is processed again
0507             // in edit mode. Since entering edit mode selects all text, the new
0508             // event will replace the text by the new key text
0509             default:
0510                 if (!isEditModeActive() && e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) {
0511                     startEditing();
0512                     qApp->postEvent(m_q, new QKeyEvent(QEvent::KeyPress, e->key(), e->modifiers(), e->text(), e->isAutoRepeat()));
0513                     return true;
0514                 }
0515                 break;
0516         }
0517         return false;
0518     }
0519 
0520     bool qContextMenuEvent(QContextMenuEvent *e)
0521     {
0522         // Shows a menu. Code inspired by the QAbstractSpinBox
0523         // contextMenuEvent function
0524         
0525         // Return if we are in slider mode and the mouse is in the line edit
0526         if (!isEditModeActive() && m_lineEdit->rect().contains(e->pos())) {
0527             return true;
0528         }
0529         // Create the menu
0530         QPointer<QMenu> menu;
0531         // If we are in edit mode, then add the line edit
0532         // actions
0533         if (isEditModeActive()) {
0534             menu = m_lineEdit->createStandardContextMenu();
0535             m_focusLostDueToMenu = true;
0536         } else {
0537             menu = new QMenu;
0538         }
0539         if (!menu) {
0540             return true;
0541         }
0542         // Override select all action
0543         QAction *selectAllAction = nullptr;
0544         if (isEditModeActive()) {
0545             selectAllAction = new QAction(i18nc("Menu action to select all text in the slider spin box", "&Select All"), menu);
0546 #if QT_CONFIG(shortcut)
0547             selectAllAction->setShortcut(QKeySequence::SelectAll);
0548 #endif
0549             menu->removeAction(menu->actions().last());
0550             menu->addAction(selectAllAction);
0551             menu->addSeparator();
0552         }
0553         // Add step up and step down actions
0554         const uint stepEnabled = m_q->stepEnabled();
0555         QAction *stepUpAction = menu->addAction(i18nc("Menu action to step up in the slider spin box", "&Step up"));
0556         stepUpAction->setEnabled(stepEnabled & SpinBoxType::StepUpEnabled);
0557         QAction *stepDown = menu->addAction(i18nc("Menu action to step down in the slider spin box", "Step &down"));
0558         stepDown->setEnabled(stepEnabled & SpinBoxType::StepDownEnabled);
0559         menu->addSeparator();
0560         // This code is taken from QAbstractSpinBox. Use a QPointer in case the
0561         // spin box is destroyed while the menu is shown??
0562         const QPointer<SpinBoxType> spinbox = m_q;
0563         const QPoint pos =
0564             (e->reason() == QContextMenuEvent::Mouse)
0565             ? e->globalPos()
0566             : m_q->mapToGlobal(QPoint(e->pos().x(), 0)) + QPoint(m_q->width() / 2, m_q->height() / 2);
0567         const QAction *action = menu->exec(pos);
0568         delete static_cast<QMenu *>(menu);
0569         if (spinbox && action) {
0570             if (action == stepUpAction) {
0571                 m_q->stepBy(static_cast<ValueType>(1));
0572             } else if (action == stepDown) {
0573                 m_q->stepBy(static_cast<ValueType>(-1));
0574             } else if (action == selectAllAction) {
0575                 m_q->selectAll();
0576             }
0577         }
0578         e->accept();
0579         return true;
0580     }
0581 
0582     // Generic "style aware" helper function to draw a rect
0583     void paintSliderRect(QPainter &painter, const QRectF &rect, const QBrush &brush)
0584     {
0585         painter.save();
0586         painter.setBrush(brush);
0587         painter.setPen(Qt::NoPen);
0588         if (qApp->property(currentUnderlyingStyleNameProperty).toString().toLower() == "fusion") {
0589             painter.drawRoundedRect(rect, 1, 1);
0590         } else {
0591             painter.drawRoundedRect(rect, 0, 0);
0592         }
0593         painter.restore();
0594     }
0595 
0596     void paintSliderText(QPainter &painter, const QString &text, const QRectF &rect, const QRectF &clipRect, const QColor &color, const QTextOption &textOption)
0597     {
0598         painter.setBrush(Qt::NoBrush);
0599         painter.setPen(color);
0600         painter.setClipping(true);
0601         painter.setClipRect(clipRect);
0602         painter.drawText(rect, text, textOption);
0603         painter.setClipping(false);
0604     };
0605 
0606     void paintGenericSliderText(QPainter &painter, const QString &text, const QRectF &rect, const QRectF &sliderRect)
0607     {
0608         QTextOption textOption(Qt::AlignAbsolute | Qt::AlignHCenter | Qt::AlignVCenter);
0609         textOption.setWrapMode(QTextOption::NoWrap);
0610         // Draw portion of the text that is over the background
0611         paintSliderText(painter, text, rect, rect.adjusted(sliderRect.width(), 0, 0, 0), m_lineEdit->palette().text().color(), textOption);
0612         // Draw portion of the text that is over the progress bar
0613         paintSliderText(painter, text, rect, sliderRect, m_lineEdit->palette().highlightedText().color(), textOption);
0614     };
0615 
0616     void paintSlider(QPainter &painter, const QString &text, double slider01Width, double slider02Width = -1.0)
0617     {
0618         const QRectF rect = m_lineEdit->rect();
0619         const QColor highlightColor = m_q->palette().highlight().color();
0620         if (slider02Width < 0.0) {
0621             const QRectF sliderRect = rect.adjusted(0, 0, -(rect.width() - slider01Width), 0);
0622             paintSliderRect(painter, sliderRect, highlightColor);
0623             if (!text.isNull()) {
0624                 paintGenericSliderText(painter, text, rect, sliderRect);
0625             }
0626         } else {
0627             static constexpr double heightOfCollapsedSliderPlusSpace = heightOfCollapsedSlider + heightOfSpaceBetweenSliders;
0628             const double heightBetween = rect.height() - 2.0 * heightOfCollapsedSlider - heightOfSpaceBetweenSliders;
0629             const double animationPos = m_sliderAnimation.currentValue().toReal();
0630             const double a = heightOfCollapsedSliderPlusSpace;
0631             const double b = heightOfCollapsedSliderPlusSpace + heightBetween;
0632             // Paint background text
0633             QTextOption textOption(Qt::AlignAbsolute | Qt::AlignHCenter | Qt::AlignVCenter);
0634             textOption.setWrapMode(QTextOption::NoWrap);
0635             paintSliderText(painter, text, rect, rect, m_lineEdit->palette().text().color(), textOption);
0636             // Paint soft range slider
0637             const QColor softSliderColor = KisPaintingTweaks::blendColors(highlightColor, m_q->palette().base().color(), 1.0 - 0.25);
0638             const double softSliderAdjustment = -KisAlgebra2D::lerp(a, b, animationPos);
0639             paintSliderRect(
0640                 painter,
0641                 rect.adjusted(0, 0, -(rect.width() - slider02Width), softSliderAdjustment),
0642                 softSliderColor
0643             );
0644             // Paint hard range slider
0645             const double hardSliderAdjustment = KisAlgebra2D::lerp(a, b, 1.0 - animationPos);
0646             paintSliderRect(
0647                 painter,
0648                 rect.adjusted(0, hardSliderAdjustment, -(rect.width() - slider01Width), 0),
0649                 highlightColor
0650             );
0651             // Paint text in the sliders
0652             paintSliderText(
0653                 painter, text, rect,
0654                 rect.adjusted(0, 0, -(rect.width() - slider02Width), softSliderAdjustment),
0655                 m_lineEdit->palette().highlightedText().color(),
0656                 textOption
0657             );
0658             paintSliderText(
0659                 painter, text, rect,
0660                 rect.adjusted(0, hardSliderAdjustment, -(rect.width() - slider01Width), 0),
0661                 m_lineEdit->palette().highlightedText().color(),
0662                 textOption
0663             );
0664         }
0665     }
0666 
0667     double computeSliderWidth(double min, double max, double value) const
0668     {
0669         const double rangeSize = max - min;
0670         const double localPosition = value - min;
0671         const double normalizedValue = std::pow(localPosition / rangeSize, 1.0 / m_exponentRatio);
0672         const double width = static_cast<double>(m_lineEdit->width());
0673         return qBound(0.0, std::round(normalizedValue * width), width);
0674     }
0675 
0676     bool lineEditPaintEvent(QPaintEvent*)
0677     {
0678         QPainter painter(m_lineEdit);
0679         painter.setRenderHint(QPainter::Antialiasing, true);
0680 
0681         const double value = m_q->value();
0682         
0683         // If we are not editing, just draw the text, otherwise draw a 
0684         // semi-transparent rect to dim the background and let the QLineEdit
0685         // draw the rest (text, selection, cursor, etc.)
0686         const double hardSliderWidth = computeSliderWidth(static_cast<double>(m_q->minimum()), static_cast<double>(m_q->maximum()), value);
0687         const double softSliderWidth = computeSliderWidth(m_softMinimum, m_softMaximum, value);
0688         if (!isEditModeActive()) {
0689             QString text = m_q->text();
0690             if (isSoftRangeValid()) {
0691                 if (m_softRangeViewMode == SoftRangeViewMode_AlwaysShowSoftRange) {
0692                     paintSlider(painter, text, softSliderWidth);
0693                 } else {
0694                     paintSlider(painter, text, hardSliderWidth, softSliderWidth);
0695                 }
0696             } else {
0697                 // Draw the slider
0698                 paintSlider(painter, text, hardSliderWidth);
0699             }
0700         } else {
0701             // Draw the slider
0702             if (isSoftRangeValid()) {
0703                 if (m_softRangeViewMode == SoftRangeViewMode_AlwaysShowSoftRange) {
0704                     paintSlider(painter, QString(), softSliderWidth);
0705                 } else {
0706                     paintSlider(painter, QString(), hardSliderWidth, softSliderWidth);
0707                 }
0708             } else {
0709                 paintSlider(painter, QString(), hardSliderWidth);
0710             }
0711             // Paint the overlay with the base color
0712             QColor color = m_q->palette().base().color();
0713             color.setAlpha(128);
0714             paintSliderRect(painter, m_lineEdit->rect(), color);
0715         }
0716         // If we are editing the text then return false and let the QLineEdit
0717         // paint all the edit related stuff (e.g. selection)
0718         return !isEditModeActive();
0719     }
0720 
0721     bool lineEditMousePressEvent(QMouseEvent *e)
0722     {
0723         if (!m_q->isEnabled()) {
0724             return false;
0725         }
0726         if (!isEditModeActive()) {
0727             // Pressing and holding the left button in the lineedit in slider
0728             // mode starts a timer which makes the lineedit enter
0729             // edition mode if it is completed
0730             if (e->button() == Qt::LeftButton) {
0731                 m_lastMousePressPosition = e->pos();
0732                 const QPoint currentValuePosition = pointForValue(m_q->value());
0733                 m_relativeDraggingOffset = currentValuePosition.x() - e->x();
0734                 m_useRelativeDragging = (e->modifiers() & Qt::ShiftModifier)
0735                                         || qAbs(m_relativeDraggingOffset) <= relativeDraggingMargin;
0736                 m_timerStartEditing.start(qApp->styleHints()->mousePressAndHoldInterval());
0737             }
0738             return true;
0739         }
0740         return false;
0741     }
0742 
0743     bool lineEditMouseReleaseEvent(QMouseEvent *e)
0744     {
0745         if (!m_q->isEnabled()) {
0746             return false;
0747         }
0748         if (!isEditModeActive()) {
0749             // Releasing the right mouse button makes the lineedit enter
0750             // the edition mode if we are not editing
0751             if (e->button() == Qt::RightButton) {
0752                 // If we call startEditing() right from the eventFilter(),
0753                 // then the mouse release event will be somehow be passed
0754                 // to Qt further and generate ContextEvent on Windows.
0755                 // Therefore we should call it from a normal timer event.
0756                 QTimer::singleShot(0, &m_startEditingSignalProxy, SLOT(start()));
0757             // Releasing the left mouse button stops the dragging and also
0758             // the "enter edition mode" timer. If signals must be blocked when
0759             // dragging then we set the value here and emit a signal
0760             } else if (e->button() == Qt::LeftButton) {
0761                 m_timerStartEditing.stop();
0762 
0763                 if (m_blockUpdateSignalOnDrag) {
0764                     const QPoint p(m_useRelativeDragging ? e->pos().x() + m_relativeDraggingOffset : e->pos().x(),
0765                                    e->pos().y());
0766                     setValue(valueForPoint(p, e->modifiers()), false, true);
0767                 } else {
0768                     if (!m_isDragging) {
0769                         setValue(valueForPoint(e->pos(), e->modifiers()), false, true);
0770                     }
0771                 }
0772 
0773                 m_isDragging = false;
0774                 emit m_q->draggingFinished();
0775             }
0776             return true;
0777         }
0778         return false;
0779     }
0780 
0781     bool lineEditMouseMoveEvent(QMouseEvent *e)
0782     {
0783         if (!m_q->isEnabled()) {
0784             return false;
0785         }
0786         if (!isEditModeActive()) {
0787             if (e->buttons() & Qt::LeftButton) {
0788                 // If the timer is active that means we pressed the button in
0789                 // slider mode
0790                 if (m_timerStartEditing.isActive()) {
0791                     const int dx = e->pos().x() - m_lastMousePressPosition.x();
0792                     const int dy = e->pos().y() - m_lastMousePressPosition.y();
0793                     // If the mouse position is still close to the point where
0794                     // we pressed, then we still wait for the "enter edit mode"
0795                     // timer to complete
0796                     if (dx * dx + dy * dy <= startDragDistanceSquared) {
0797                         return true;
0798                     // If the mouse moved far from where we first pressed, then
0799                     // stop the timer and start dragging
0800                     } else {
0801                         m_timerStartEditing.stop();
0802                         m_isDragging = true;
0803                     }
0804                 }
0805                 // At this point we are dragging so record the position and set
0806                 // the value
0807                 const QPoint p(m_useRelativeDragging ? e->pos().x() + m_relativeDraggingOffset : e->pos().x(),
0808                                e->pos().y());
0809                 setValue(valueForPoint(p, e->modifiers()), m_blockUpdateSignalOnDrag);
0810                 return true;
0811             }
0812         }
0813         return false;
0814     }
0815 
0816     bool widgetRangeTogglePaintEvent(QPaintEvent*)
0817     {
0818         QPainter painter(m_widgetRangeToggle);
0819         painter.setRenderHint(QPainter::Antialiasing, true);
0820         // Compute sizes and positions
0821         const double width = static_cast<double>(m_widgetRangeToggle->width());
0822         const double height = static_cast<double>(m_widgetRangeToggle->height());
0823         constexpr double marginX = 4.0;
0824         const double toggleWidth = width - 2.0 * marginX;
0825         const double centerX = width * 0.5;
0826         const double centerY = height * 0.5;
0827         const double bigRadius = centerX - std::floor(centerX - (toggleWidth * 0.5)) + 0.5;
0828         const double smallRadius = bigRadius * 0.5;
0829         const double sliderAnimationPos = m_sliderAnimation.currentValue().toReal();
0830         const double radius = smallRadius + sliderAnimationPos * (bigRadius - smallRadius);
0831         // Compute color
0832         const double rangeToggleHoverAnimationPos = m_rangeToggleHoverAnimation.currentValue().toReal();
0833         const QColor baseColor = m_q->palette().base().color();
0834         const QColor textColor = m_q->palette().text().color();
0835         const QColor color = KisPaintingTweaks::blendColors(baseColor, textColor, 1.0 - (0.60 + 0.40 * rangeToggleHoverAnimationPos));
0836         // Paint outer circle
0837         painter.setPen(color);
0838         painter.setBrush(Qt::NoBrush);
0839         painter.drawEllipse(QPointF(centerX, centerY), bigRadius, bigRadius);
0840         // Paint dot
0841         painter.setPen(Qt::NoPen);
0842         painter.setBrush(color);
0843         painter.drawEllipse(QPointF(centerX, centerY), radius, radius);
0844         return true;
0845     }
0846 
0847     bool widgetRangeToggletMouseReleaseEvent(QMouseEvent *e)
0848     {
0849         if (!m_q->isEnabled()) {
0850             return false;
0851         }
0852         if (e->button() == Qt::LeftButton) {
0853             if (!m_isSoftRangeActive) {
0854                 makeSoftRangeActive();
0855             } else {
0856                 makeHardRangeActive();
0857             }
0858             return true;
0859         }
0860         return false;
0861     }
0862 
0863     bool widgetRangeToggleEnterEvent(QEvent*)
0864     {
0865         m_rangeToggleHoverAnimation.stop();
0866         // scale the animation duration in case the animation is in the middle
0867         const int animationDuration =
0868             static_cast<int>(std::round(m_rangeToggleHoverAnimation.currentValue().toReal() * fullAnimationDuration));
0869         m_rangeToggleHoverAnimation.setStartValue(m_rangeToggleHoverAnimation.currentValue());
0870         m_rangeToggleHoverAnimation.setEndValue(1.0);
0871         m_rangeToggleHoverAnimation.setDuration(animationDuration);
0872         m_rangeToggleHoverAnimation.start();
0873         return false;
0874     }
0875 
0876     bool widgetRangeToggleLeaveEvent(QEvent*)
0877     {
0878         m_rangeToggleHoverAnimation.stop();
0879         // scale the animation duration in case the animation is in the middle
0880         const int animationDuration =
0881             static_cast<int>(std::round(m_rangeToggleHoverAnimation.currentValue().toReal() * fullAnimationDuration));
0882         m_rangeToggleHoverAnimation.setStartValue(m_rangeToggleHoverAnimation.currentValue());
0883         m_rangeToggleHoverAnimation.setEndValue(0.0);
0884         m_rangeToggleHoverAnimation.setDuration(animationDuration);
0885         m_rangeToggleHoverAnimation.start();
0886         return false;
0887     }
0888 
0889     bool eventFilter(QObject * o, QEvent * e) override
0890     {
0891         if (!o || !e) {
0892             return false;
0893         }
0894         if (o == m_q) {
0895             switch (e->type()) {
0896                 case QEvent::Resize : return qResizeEvent(static_cast<QResizeEvent*>(e));
0897                 case QEvent::FocusOut : return qFocusOutEvent(static_cast<QFocusEvent*>(e));
0898                 case QEvent::MouseButtonPress : return qMousePressEvent(static_cast<QMouseEvent*>(e));
0899                 case QEvent::KeyPress : return qKeyPressEvent(static_cast<QKeyEvent*>(e));
0900                 case QEvent::ContextMenu : return qContextMenuEvent(static_cast<QContextMenuEvent*>(e));
0901                 default: break;
0902             }
0903         } else if (o == m_lineEdit) {
0904             switch (e->type()) {
0905                 case QEvent::Paint : return lineEditPaintEvent(static_cast<QPaintEvent*>(e));
0906                 case QEvent::MouseButtonPress : return lineEditMousePressEvent(static_cast<QMouseEvent*>(e));
0907                 case QEvent::MouseButtonRelease : return lineEditMouseReleaseEvent(static_cast<QMouseEvent*>(e));
0908                 case QEvent::MouseMove : return lineEditMouseMoveEvent(static_cast<QMouseEvent*>(e));
0909                 default: break;
0910             }
0911         } else if (o == m_widgetRangeToggle) {
0912             switch (e->type()) {
0913                 case QEvent::Paint : return widgetRangeTogglePaintEvent(static_cast<QPaintEvent*>(e));
0914                 case QEvent::MouseButtonRelease: return widgetRangeToggletMouseReleaseEvent(static_cast<QMouseEvent*>(e));
0915                 case QEvent::Enter: return widgetRangeToggleEnterEvent(e);
0916                 case QEvent::Leave: return widgetRangeToggleLeaveEvent(e);
0917                 default: break;
0918             }
0919         }
0920         return false;
0921     }
0922 
0923 private:
0924     // Distance that the pointer must move to start dragging
0925     static constexpr int startDragDistance{2};
0926     static constexpr int startDragDistanceSquared{startDragDistance * startDragDistance};
0927     // Margin around the spinbox for which the dragging gives same results,
0928     // regardless of the vertical distance
0929     static constexpr double constantDraggingMargin{32.0};
0930     // Margin around the current value that marks if the dragging should be
0931     // relative to the current value (inside the margin) or absolute (outside)
0932     static constexpr int relativeDraggingMargin{15};
0933     // Height of the collapsed slider bar
0934     static constexpr double heightOfCollapsedSlider{3.0};
0935     // Height of the space between the soft and hard range sliders
0936     static constexpr double heightOfSpaceBetweenSliders{0.0};
0937     // Width of the area to activate soft/hard range
0938     static constexpr double widthOfRangeModeToggle{16.0};
0939     // The duration of the animation 
0940     static constexpr double fullAnimationDuration{200.0};
0941 
0942     SpinBoxType *m_q {nullptr};
0943     QLineEdit *m_lineEdit {nullptr};
0944     QWidget *m_widgetRangeToggle {nullptr};
0945     QTimer m_timerStartEditing;
0946     ValueType m_softMinimum {static_cast<ValueType>(0)};
0947     ValueType m_softMaximum {static_cast<ValueType>(0)};
0948     double m_exponentRatio {1.0};
0949     bool m_blockUpdateSignalOnDrag {false};
0950     ValueType m_fastSliderStep {static_cast<ValueType>(5)};
0951     mutable ValueType m_valueBeforeEditing {static_cast<ValueType>(0)};
0952     bool m_isDragging {false};
0953     bool m_useRelativeDragging {false};
0954     int m_relativeDraggingOffset {0};
0955     QPoint m_lastMousePressPosition;
0956     int m_rightClickCounter {0};
0957     bool m_focusLostDueToMenu {false};
0958     bool m_isSoftRangeActive {true};
0959     QVariantAnimation m_sliderAnimation;
0960     QVariantAnimation m_rangeToggleHoverAnimation;
0961     SignalToFunctionProxy m_startEditingSignalProxy;
0962 
0963     enum SoftRangeViewMode
0964     {
0965         SoftRangeViewMode_AlwaysShowSoftRange,
0966         SoftRangeViewMode_ShowBothRanges
0967     } m_softRangeViewMode{SoftRangeViewMode_ShowBothRanges};
0968 };
0969 
0970 #endif // KISSLIDERSPINBOXPRIVATE_H