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