File indexing completed on 2024-05-12 04:44:34
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 // Own headers 0005 // First the interface, which forces the header to be self-contained. 0006 #include "multispinbox.h" 0007 // Second, the private implementation. 0008 #include "multispinbox_p.h" // IWYU pragma: associated 0009 0010 #include "constpropagatingrawpointer.h" 0011 #include "constpropagatinguniquepointer.h" 0012 #include "extendeddoublevalidator.h" 0013 #include "helpermath.h" 0014 #include "multispinboxsection.h" 0015 #include <math.h> 0016 #include <qaccessible.h> 0017 #include <qaccessiblewidget.h> 0018 #include <qcoreevent.h> 0019 #include <qdebug.h> 0020 #include <qevent.h> 0021 #include <qfontmetrics.h> 0022 #include <qglobal.h> 0023 #include <qlineedit.h> 0024 #include <qlocale.h> 0025 #include <qnamespace.h> 0026 #include <qobject.h> 0027 #include <qpointer.h> 0028 #include <qstringbuilder.h> 0029 #include <qstringliteral.h> 0030 #include <qstyle.h> 0031 #include <qstyleoption.h> 0032 #include <qwidget.h> 0033 class QAction; 0034 0035 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0036 #include <qobjectdefs.h> 0037 #else 0038 #endif 0039 0040 namespace PerceptualColor 0041 { 0042 /** @brief If the text cursor is touching at the current section’s value. 0043 * 0044 * Everything from the cursor position exactly before the value itself up 0045 * to the cursor position exactly after the value itself. Prefixes and 0046 * suffixes are not considered as part of the value. Example: “ab12cd” 0047 * (prefix “ab”, value 12, suffix “cd”). The cursor positions 2, 3 and 4 are 0048 * considered <em>touching</em> the current value. 0049 * 0050 * @returns <tt>true</tt> if the text cursor is touching at the current 0051 * section’s value.. <tt>false</tt> otherwise. */ 0052 bool MultiSpinBoxPrivate::isCursorTouchingCurrentSectionValue() const 0053 { 0054 const auto cursorPosition = q_pointer->lineEdit()->cursorPosition(); 0055 const bool highEnough = (cursorPosition >= m_textBeforeCurrentValue.length()); 0056 const auto after = q_pointer->lineEdit()->text().length() // 0057 - m_textAfterCurrentValue.length(); 0058 const bool lowEnough = (cursorPosition <= after); 0059 return (highEnough && lowEnough); 0060 } 0061 0062 /** @brief The recommended minimum size for the widget 0063 * 0064 * Reimplemented from base class. 0065 * 0066 * @returns the recommended minimum size for the widget 0067 * 0068 * @internal 0069 * 0070 * @sa @ref sizeHint() 0071 * 0072 * @note The minimum size of the widget is the same as @ref sizeHint(). This 0073 * behaviour is different from <tt>QSpinBox</tt> and <tt>QDoubleSpinBox</tt> 0074 * that have a minimum size hint that allows for displaying only prefix and 0075 * value, but not the suffix. However, such a behavior does not seem 0076 * appropriate for a @ref MultiSpinBox because it could be confusing, given 0077 * that its content is more complex. */ 0078 QSize MultiSpinBox::minimumSizeHint() const 0079 { 0080 return sizeHint(); 0081 } 0082 0083 /** @brief Constructor 0084 * 0085 * @param parent the parent widget, if any */ 0086 MultiSpinBox::MultiSpinBox(QWidget *parent) 0087 : QAbstractSpinBox(parent) 0088 , d_pointer(new MultiSpinBoxPrivate(this)) 0089 { 0090 // Set up the m_validator 0091 d_pointer->m_validator = new ExtendedDoubleValidator(this); 0092 d_pointer->m_validator->setLocale(locale()); 0093 lineEdit()->setValidator(d_pointer->m_validator); 0094 0095 // Initialize the configuration (default: only one section). 0096 // This will also change section values to exactly one element. 0097 setSectionConfigurations(QList<MultiSpinBoxSection>{MultiSpinBoxSection()}); 0098 setSectionValues(QList<double>{MultiSpinBoxPrivate::defaultSectionValue}); 0099 d_pointer->m_currentIndex = -1; // This will force 0100 // setCurrentIndexAndUpdateTextAndSelectValue() 0101 // to really apply the changes, including updating 0102 // the validator: 0103 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0); 0104 0105 // Connect signals and slots 0106 connect(lineEdit(), // sender 0107 &QLineEdit::textChanged, // signal 0108 d_pointer.get(), // receiver 0109 &MultiSpinBoxPrivate::updateCurrentValueFromText // slot 0110 ); 0111 connect(lineEdit(), // sender 0112 &QLineEdit::cursorPositionChanged, // signal 0113 d_pointer.get(), // receiver 0114 &MultiSpinBoxPrivate::reactOnCursorPositionChange // slot 0115 ); 0116 connect(this, // sender 0117 &QAbstractSpinBox::editingFinished, // signal 0118 d_pointer.get(), // receiver 0119 &MultiSpinBoxPrivate::setCurrentIndexToZeroAndUpdateTextAndSelectValue // slot 0120 ); 0121 0122 // Initialize accessibility support 0123 QAccessible::installFactory( 0124 // It’s safe to call installFactory() multiple times with the 0125 // same factory. If the factory is yet installed, it will not 0126 // be installed again. 0127 &AccessibleMultiSpinBox::factory); 0128 } 0129 0130 /** @brief Default destructor */ 0131 MultiSpinBox::~MultiSpinBox() noexcept 0132 { 0133 } 0134 0135 /** @brief Constructor 0136 * 0137 * @param backLink Pointer to the object from which <em>this</em> object 0138 * is the private implementation. */ 0139 MultiSpinBoxPrivate::MultiSpinBoxPrivate(MultiSpinBox *backLink) 0140 : q_pointer(backLink) 0141 { 0142 } 0143 0144 /** @brief The recommended size for the widget 0145 * 0146 * Reimplemented from base class. 0147 * 0148 * @returns the size hint 0149 * 0150 * @internal 0151 * 0152 * @note Some widget styles like CDE and Motif calculate badly (too small) 0153 * the size for QAbstractSpinBox and its child classes, and therefore also 0154 * for this widget. 0155 * 0156 * @sa @ref minimumSizeHint() */ 0157 QSize MultiSpinBox::sizeHint() const 0158 { 0159 // This function intentionally does not cache the text string. 0160 // Which variant is the longest text string, that depends on the current 0161 // font policy. This might have changed since the last call. Therefore, 0162 // each time this function is called, we calculate again the longest 0163 // test string (“completeString”). 0164 0165 ensurePolished(); 0166 0167 const QFontMetrics myFontMetrics(fontMetrics()); 0168 QList<MultiSpinBoxSection> myConfiguration = d_pointer->m_sectionConfigurations; 0169 const int height = lineEdit()->sizeHint().height(); 0170 int width = 0; 0171 QString completeString; 0172 0173 // Get the text for all the sections 0174 for (int i = 0; i < myConfiguration.count(); ++i) { 0175 // Prefix 0176 completeString += myConfiguration.at(i).prefix(); 0177 // For each section, test if the minimum value or the maximum 0178 // takes more space (width). Choose the one that takes more place 0179 // (width). 0180 const QString textOfMinimumValue = locale().toString( // 0181 myConfiguration.at(i).minimum(), // value 0182 'f', // format 0183 myConfiguration.at(i).decimals() // precision 0184 ); 0185 const QString textOfMaximumValue = locale().toString( // 0186 myConfiguration.at(i).maximum(), // value 0187 'f', // format 0188 myConfiguration.at(i).decimals() // precision 0189 ); 0190 if (myFontMetrics.horizontalAdvance(textOfMinimumValue) > myFontMetrics.horizontalAdvance(textOfMaximumValue)) { 0191 completeString += textOfMinimumValue; 0192 } else { 0193 completeString += textOfMaximumValue; 0194 } 0195 // Suffix 0196 completeString += myConfiguration.at(i).suffix(); 0197 } 0198 0199 // Add some extra space, just as QSpinBox seems to do also. 0200 completeString += QStringLiteral(u" "); 0201 0202 // Calculate string width and add two extra pixel for cursor 0203 // blinking space. 0204 width = myFontMetrics.horizontalAdvance(completeString) + 2; 0205 0206 // Calculate the final size in pixel 0207 QStyleOptionSpinBox myStyleOptionsForSpinBoxes; 0208 initStyleOption(&myStyleOptionsForSpinBoxes); 0209 myStyleOptionsForSpinBoxes.buttonSymbols = QAbstractSpinBox::PlusMinus; 0210 0211 const QSize contentSize(width, height); 0212 // Calculate widget size necessary to display a given content 0213 QSize result = style()->sizeFromContents( 0214 // In the Kvantum style in version 0.18, there was a bug that returned 0215 // via QStyle::sizeFromContents() a width that was too small. In 0216 // Kvantum version 1.0.1 this is fixed. 0217 QStyle::CT_SpinBox, // type 0218 &myStyleOptionsForSpinBoxes, // style options 0219 contentSize, // size of the content 0220 this // optional widget argument (for better calculations) 0221 ); 0222 0223 if (d_pointer->m_actionButtonCount > 0) { 0224 // Determine the size of icons for actions similar to what Qt 0225 // does in QLineEditPrivate::sideWidgetParameters() and than 0226 // add this to the size hint. 0227 const int actionButtonIconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, // pixel metric type 0228 nullptr, // style options 0229 lineEdit() // widget (optional) 0230 ); 0231 const int actionButtonMargin = actionButtonIconSize / 4; 0232 const int actionButtonWidth = actionButtonIconSize + 6; 0233 // Only 1 margin per button: 0234 const int actionButtonSpace = actionButtonWidth + actionButtonMargin; 0235 result.setWidth(result.width() + d_pointer->m_actionButtonCount * actionButtonSpace); 0236 } 0237 0238 return result; 0239 } 0240 0241 /** @brief Handle state changes 0242 * 0243 * Implements reaction on <tt>QEvent::LanguageChange</tt>. 0244 * 0245 * Reimplemented from base class. 0246 * 0247 * @param event The event to process */ 0248 void MultiSpinBox::changeEvent(QEvent *event) 0249 { 0250 // QEvent::StyleChange or QEvent::FontChange are not handled here 0251 // because they trigger yet a content and geometry update in the 0252 // base class’s implementation of this function. 0253 if ( // 0254 (event->type() == QEvent::LanguageChange) // 0255 || (event->type() == QEvent::LocaleChange) // 0256 // The base class’s implementation for QEvent::LayoutDirectionChange 0257 // would only call update, not updateGeometry… 0258 || (event->type() == QEvent::LayoutDirectionChange) // 0259 ) { 0260 // Updates the widget content and its geometry 0261 update(); 0262 updateGeometry(); 0263 } 0264 QAbstractSpinBox::changeEvent(event); 0265 } 0266 0267 /** @brief Adds to the widget a button associated with the given action. 0268 * 0269 * The icon of the action will be displayed as button. If the action has 0270 * no icon, just an empty space will be displayed. 0271 * 0272 * @image html MultiSpinBoxWithButton.png "MultiSpinBox with action button" width=200 0273 * 0274 * It is possible to add more than one action. 0275 * 0276 * @param action This action that will be executed when clicking the button. 0277 * (The ownership of the action object remains unchanged.) 0278 * @param position The position of the button within the widget (left 0279 * or right) 0280 * @note See @ref hidpisupport "High DPI support" about how to enable 0281 * support for high-DPI icons. 0282 * @note The action will <em>not</em> appear in the 0283 * <tt>QWidget::actions()</tt> function of this class. */ 0284 void MultiSpinBox::addActionButton(QAction *action, QLineEdit::ActionPosition position) 0285 { 0286 lineEdit()->addAction(action, position); 0287 d_pointer->m_actionButtonCount += 1; 0288 // The size hints have changed, because an additional button needs 0289 // more space. 0290 updateGeometry(); 0291 } 0292 0293 /** @brief Get formatted value for a given section. 0294 * @param index The index of the section 0295 * @returns The value of the given section, formatted (without prefix or 0296 * suffix), as text. */ 0297 QString MultiSpinBoxPrivate::formattedValue(QListSizeType index) const 0298 { 0299 return q_pointer->locale().toString( 0300 // The value to be formatted: 0301 q_pointer->sectionValues().at(index), 0302 // Format as floating point with decimal digits 0303 'f', 0304 // Number of decimal digits 0305 m_sectionConfigurations.at(index).decimals()); 0306 } 0307 0308 /** @brief Updates prefix, value and suffix text 0309 * 0310 * @pre <tt>0 <= @ref m_currentIndex < @ref m_sectionConfigurations .count()</tt> 0311 * 0312 * @post Updates @ref m_textBeforeCurrentValue, @ref m_textOfCurrentValue, 0313 * @ref m_textAfterCurrentValue to the correct values based 0314 * on @ref m_currentIndex. */ 0315 void MultiSpinBoxPrivate::updatePrefixValueSuffixText() 0316 { 0317 QListSizeType i; 0318 0319 // Update m_currentSectionTextBeforeValue 0320 m_textBeforeCurrentValue = QString(); 0321 for (i = 0; i < m_currentIndex; ++i) { 0322 m_textBeforeCurrentValue.append(m_sectionConfigurations.at(i).prefix()); 0323 m_textBeforeCurrentValue.append(formattedValue(i)); 0324 m_textBeforeCurrentValue.append(m_sectionConfigurations.at(i).suffix()); 0325 } 0326 m_textBeforeCurrentValue.append(m_sectionConfigurations.at(m_currentIndex).prefix()); 0327 0328 // Update m_currentSectionTextOfTheValue 0329 m_textOfCurrentValue = formattedValue(m_currentIndex); 0330 0331 // Update m_currentSectionTextAfterValue 0332 m_textAfterCurrentValue = QString(); 0333 m_textAfterCurrentValue.append(m_sectionConfigurations.at(m_currentIndex).suffix()); 0334 for (i = m_currentIndex + 1; i < m_sectionConfigurations.count(); ++i) { 0335 m_textAfterCurrentValue.append(m_sectionConfigurations.at(i).prefix()); 0336 0337 m_textAfterCurrentValue.append(formattedValue(i)); 0338 m_textAfterCurrentValue.append(m_sectionConfigurations.at(i).suffix()); 0339 } 0340 } 0341 0342 /** @brief Sets the current section index to <tt>0</tt>. 0343 * 0344 * Convenience function that simply calls 0345 * @ref setCurrentIndexAndUpdateTextAndSelectValue with the 0346 * argument <tt>0</tt>. */ 0347 void MultiSpinBoxPrivate::setCurrentIndexToZeroAndUpdateTextAndSelectValue() 0348 { 0349 setCurrentIndexAndUpdateTextAndSelectValue(0); 0350 } 0351 0352 /** @brief Sets the current section index. 0353 * 0354 * Updates the text in the QLineEdit of this widget. If the widget has focus, 0355 * it also selects the value of the new current section. 0356 * 0357 * @param newIndex The index of the new current section. Must be a valid 0358 * index. The update will be done even if this argument is identical to 0359 * the @ref m_currentIndex. 0360 * 0361 * @sa @ref setCurrentIndexToZeroAndUpdateTextAndSelectValue 0362 * @sa @ref setCurrentIndexWithoutUpdatingText */ 0363 void MultiSpinBoxPrivate::setCurrentIndexAndUpdateTextAndSelectValue(QListSizeType newIndex) 0364 { 0365 QSignalBlocker myBlocker(q_pointer->lineEdit()); 0366 setCurrentIndexWithoutUpdatingText(newIndex); 0367 // Update the line edit widget 0368 q_pointer->lineEdit()->setText(m_textBeforeCurrentValue // 0369 + m_textOfCurrentValue // 0370 + m_textAfterCurrentValue); 0371 const int lengthOfTextBeforeCurrentValue = // 0372 static_cast<int>(m_textBeforeCurrentValue.length()); 0373 const int lengthOfTextOfCurrentValue = // 0374 static_cast<int>(m_textOfCurrentValue.length()); 0375 if (q_pointer->hasFocus()) { 0376 q_pointer->lineEdit()->setSelection( // 0377 lengthOfTextBeforeCurrentValue, // 0378 lengthOfTextOfCurrentValue); 0379 } else { 0380 q_pointer->lineEdit()->setCursorPosition( // 0381 lengthOfTextBeforeCurrentValue + lengthOfTextOfCurrentValue); 0382 } 0383 // Make sure that the buttons for step up and step down are updated. 0384 q_pointer->update(); 0385 } 0386 0387 /** @brief Sets the current section index without updating 0388 * the <tt>QLineEdit</tt>. 0389 * 0390 * Does not change neither the text nor the cursor in the <tt>QLineEdit</tt>. 0391 * 0392 * @param newIndex The index of the new current section. Must be a valid index. 0393 * 0394 * @sa @ref setCurrentIndexAndUpdateTextAndSelectValue */ 0395 void MultiSpinBoxPrivate::setCurrentIndexWithoutUpdatingText(QListSizeType newIndex) 0396 { 0397 if (!isInRange<qsizetype>(0, newIndex, m_sectionConfigurations.count() - 1)) { 0398 qWarning() << "The function" << __func__ // 0399 << "in file" << __FILE__ // 0400 << "near to line" << __LINE__ // 0401 << "was called with an invalid “newIndex“ argument of" << newIndex // 0402 << "thought the valid range is currently [" << 0 << ", " << m_sectionConfigurations.count() - 1 << "]. This is a bug."; 0403 throw 0; 0404 } 0405 0406 if (newIndex == m_currentIndex) { 0407 // There is nothing to do here. 0408 return; 0409 } 0410 0411 // Apply the changes 0412 m_currentIndex = newIndex; 0413 updatePrefixValueSuffixText(); 0414 m_validator->setPrefix(m_textBeforeCurrentValue); 0415 m_validator->setSuffix(m_textAfterCurrentValue); 0416 m_validator->setRange( 0417 // Minimum: 0418 m_sectionConfigurations.at(m_currentIndex).minimum(), 0419 // Maximum: 0420 m_sectionConfigurations.at(m_currentIndex).maximum()); 0421 0422 // The state (enabled/disabled) of the buttons “Step up” and “Step down” 0423 // has to be updated. To force this, update() is called manually here: 0424 q_pointer->update(); 0425 } 0426 0427 /** @brief Virtual function that determines whether stepping up and down is 0428 * legal at any given time. 0429 * 0430 * Reimplemented from base class. 0431 * 0432 * @returns whether stepping up and down is legal */ 0433 QAbstractSpinBox::StepEnabled MultiSpinBox::stepEnabled() const 0434 { 0435 const MultiSpinBoxSection currentSectionConfiguration = d_pointer->m_sectionConfigurations.at(d_pointer->m_currentIndex); 0436 const double currentSectionValue = sectionValues().at(d_pointer->m_currentIndex); 0437 0438 // When wrapping is enabled, step up and step down are always possible. 0439 if (currentSectionConfiguration.isWrapping()) { 0440 return QAbstractSpinBox::StepEnabled(StepUpEnabled | StepDownEnabled); 0441 } 0442 0443 // When wrapping is not enabled, we have to compare the value with 0444 // maximum and minimum. 0445 QAbstractSpinBox::StepEnabled result; 0446 // Test is step up should be enabled… 0447 if (currentSectionValue < currentSectionConfiguration.maximum()) { 0448 result.setFlag(StepUpEnabled, true); 0449 } 0450 0451 // Test is step down should be enabled… 0452 if (currentSectionValue > currentSectionConfiguration.minimum()) { 0453 result.setFlag(StepDownEnabled, true); 0454 } 0455 return result; 0456 } 0457 0458 /** @brief Sets the configuration for the sections. 0459 * 0460 * The first section will be selected as current section. 0461 * 0462 * @param newSectionConfigurations Defines the new sections. The new section 0463 * count in this widget is the section count given in this list. Each section 0464 * should have valid values: <tt>@ref MultiSpinBoxSection.minimum ≤ 0465 * @ref MultiSpinBoxSection.maximum</tt>. If the @ref sectionValues are 0466 * not valid within the new section configurations, they will be fixed. 0467 * 0468 * @sa @ref sectionConfigurations() */ 0469 void MultiSpinBox::setSectionConfigurations(const QList<PerceptualColor::MultiSpinBoxSection> &newSectionConfigurations) 0470 { 0471 if (newSectionConfigurations.count() < 1) { 0472 return; 0473 } 0474 0475 // Make sure that m_currentIndex will not run out-of-bound. 0476 d_pointer->m_currentIndex = qBound(0, d_pointer->m_currentIndex, newSectionConfigurations.count()); 0477 0478 // Set new section configuration 0479 d_pointer->m_sectionConfigurations = newSectionConfigurations; 0480 0481 // Make sure the value list has the correct length and the 0482 // values are updated to the new configuration: 0483 setSectionValues(sectionValues()); 0484 0485 // As the configuration has changed, the text selection might be 0486 // undefined. Define it: 0487 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_currentIndex); 0488 0489 // Make sure that the buttons for step up and step down are updated. 0490 update(); 0491 0492 // Make sure that the geometry is updated: sizeHint() and minimumSizeHint() 0493 // both depend on the section configuration! 0494 updateGeometry(); 0495 } 0496 0497 /** @brief Returns the configuration of all sections. 0498 * 0499 * @returns the configuration of all sections. 0500 * 0501 * @sa @ref setSectionConfigurations() */ 0502 QList<PerceptualColor::MultiSpinBoxSection> MultiSpinBox::sectionConfigurations() const 0503 { 0504 return d_pointer->m_sectionConfigurations; 0505 } 0506 0507 // No documentation here (documentation of properties 0508 // and its getters are in the header) 0509 QList<double> MultiSpinBox::sectionValues() const 0510 { 0511 return d_pointer->m_sectionValues; 0512 } 0513 0514 /** @brief Sets @ref m_sectionValues without updating other things. 0515 * 0516 * Other data of this widget, including the <tt>QLineEdit</tt> text, 0517 * stays unmodified. 0518 * 0519 * @param newSectionValues The new section values. This list must have 0520 * exactly as many items as @ref MultiSpinBox::sectionConfigurations. 0521 * If the new values are not within the boundaries defined in 0522 * the @ref MultiSpinBox::sectionConfigurations, 0523 * they will be adapted before being applied. 0524 * 0525 * @post @ref m_sectionValues gets updated. The signal 0526 * @ref MultiSpinBox::sectionValuesChanged() gets emitted if the 0527 * new values are actually different from the old ones. */ 0528 void MultiSpinBoxPrivate::setSectionValuesWithoutFurtherUpdating(const QList<double> &newSectionValues) 0529 { 0530 if (newSectionValues.count() < 1) { 0531 return; 0532 } 0533 0534 const QListSizeType sectionCount = m_sectionConfigurations.count(); 0535 0536 QList<double> fixedNewSectionValues = newSectionValues; 0537 0538 // Adapt the count of values: 0539 while (fixedNewSectionValues.count() < sectionCount) { 0540 // Add elements if there are not enough: 0541 fixedNewSectionValues.append(MultiSpinBoxPrivate::defaultSectionValue); 0542 } 0543 while (fixedNewSectionValues.count() > sectionCount) { 0544 // Remove elements if there are too many: 0545 fixedNewSectionValues.removeLast(); 0546 } 0547 0548 // Make sure the new section values are 0549 // valid (minimum <= value <= maximum): 0550 MultiSpinBoxSection myConfig; 0551 double rangeWidth; 0552 double temp; 0553 for (int i = 0; i < sectionCount; ++i) { 0554 myConfig = m_sectionConfigurations.at(i); 0555 fixedNewSectionValues[i] = 0556 // Round value _before_ applying boundaries/wrapping. 0557 roundToDigits(fixedNewSectionValues.at(i), myConfig.decimals()); 0558 if (myConfig.isWrapping()) { 0559 rangeWidth = myConfig.maximum() - myConfig.minimum(); 0560 if (rangeWidth <= 0) { 0561 // This is a special case. 0562 // This happens when minimum == maximum (or 0563 // if minimum > maximum, which is invalid). 0564 fixedNewSectionValues[i] = myConfig.minimum(); 0565 } else { 0566 // floating-point modulo (fmod) operation 0567 temp = fmod( 0568 // Dividend: 0569 fixedNewSectionValues.at(i) - myConfig.minimum(), 0570 // Divisor: 0571 rangeWidth); 0572 if (temp < 0) { 0573 // Negative results shall be converted 0574 // in positive results: 0575 temp += rangeWidth; 0576 } 0577 temp += myConfig.minimum(); 0578 fixedNewSectionValues[i] = temp; 0579 } 0580 } else { 0581 fixedNewSectionValues[i] = qBound( 0582 // If there is no wrapping, simply bound: 0583 myConfig.minimum(), 0584 fixedNewSectionValues.at(i), 0585 myConfig.maximum()); 0586 } 0587 } 0588 0589 if (m_sectionValues != fixedNewSectionValues) { 0590 m_sectionValues = fixedNewSectionValues; 0591 Q_EMIT q_pointer->sectionValuesChanged(fixedNewSectionValues); 0592 } 0593 } 0594 0595 /** @brief Setter for @ref sectionValues property. 0596 * 0597 * @param newSectionValues The new section values. This list must have 0598 * exactly as many items as @ref sectionConfigurations. 0599 * 0600 * The values will be bound between 0601 * @ref MultiSpinBoxSection::minimum and 0602 * @ref MultiSpinBoxSection::maximum. Their precision will be 0603 * reduced to as many decimal places as given by 0604 * @ref MultiSpinBoxSection::decimals. */ 0605 void MultiSpinBox::setSectionValues(const QList<double> &newSectionValues) 0606 { 0607 d_pointer->setSectionValuesWithoutFurtherUpdating(newSectionValues); 0608 0609 // Update some internals… 0610 d_pointer->updatePrefixValueSuffixText(); 0611 0612 // Update the QLineEdit 0613 { // Limit scope of QSignalBlocker 0614 const QSignalBlocker blocker(lineEdit()); 0615 lineEdit()->setText(d_pointer->m_textBeforeCurrentValue // 0616 + d_pointer->m_textOfCurrentValue // 0617 + d_pointer->m_textAfterCurrentValue); // 0618 // setCurrentIndexAndUpdateTextAndSelectValue(m_currentIndex); 0619 } 0620 0621 // Make sure that the buttons for step-up and step-down are updated. 0622 update(); 0623 } 0624 0625 /** @brief Focus handling for <em>Tab</em> respectively <em>Shift+Tab</em>. 0626 * 0627 * Reimplemented from base class. 0628 * 0629 * @note If it’s about moving the focus <em>within</em> this widget, the focus 0630 * move is actually done. If it’s about moving the focus to <em>another</em> 0631 * widget, the focus move is <em>not</em> actually done. 0632 * The documentation of the base class is not very detailed. This 0633 * reimplementation does not exactly behave as the documentation of the 0634 * base class suggests. Especially, it handles directly the focus move 0635 * <em>within</em> the widget itself. This was, however, the only working 0636 * solution we found. 0637 * 0638 * @param next <tt>true</tt> stands for focus handling for <em>Tab</em>. 0639 * <tt>false</tt> stands for focus handling for <em>Shift+Tab</em>. 0640 * 0641 * @returns <tt>true</tt> if the focus has actually been moved within 0642 * this widget or if a move to another widget is possible. <tt>false</tt> 0643 * otherwise. */ 0644 bool MultiSpinBox::focusNextPrevChild(bool next) 0645 { 0646 if (next == true) { // Move focus forward (Tab) 0647 if (d_pointer->m_currentIndex < (d_pointer->m_sectionConfigurations.count() - 1)) { 0648 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_currentIndex + 1); 0649 // Make sure that the buttons for step up and step down 0650 // are updated. 0651 update(); 0652 return true; 0653 } 0654 } else { // Move focus backward (Shift+Tab) 0655 if (d_pointer->m_currentIndex > 0) { 0656 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_currentIndex - 1); 0657 // Make sure that the buttons for step up and step down 0658 // are updated. 0659 update(); 0660 return true; 0661 } 0662 } 0663 0664 // Make sure that the buttons for step up and step down are updated. 0665 update(); 0666 0667 // Return 0668 return QWidget::focusNextPrevChild(next); 0669 } 0670 0671 /** @brief Handles a <tt>QEvent::FocusOut</tt>. 0672 * 0673 * Reimplemented from base class. 0674 * 0675 * Updates the widget (except for windows that do not 0676 * specify a <tt>focusPolicy()</tt>). 0677 * 0678 * @param event the <tt>QEvent::FocusOut</tt> to be handled. */ 0679 void MultiSpinBox::focusOutEvent(QFocusEvent *event) 0680 { 0681 QAbstractSpinBox::focusOutEvent(event); 0682 switch (event->reason()) { 0683 case Qt::ShortcutFocusReason: 0684 case Qt::TabFocusReason: 0685 case Qt::BacktabFocusReason: 0686 case Qt::MouseFocusReason: 0687 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0); 0688 // Make sure that the buttons for step up and step down 0689 // are updated. 0690 update(); 0691 return; 0692 case Qt::ActiveWindowFocusReason: 0693 case Qt::PopupFocusReason: 0694 case Qt::MenuBarFocusReason: 0695 case Qt::OtherFocusReason: 0696 case Qt::NoFocusReason: 0697 default: 0698 update(); 0699 return; 0700 } 0701 } 0702 0703 /** @brief Handles a <tt>QEvent::FocusIn</tt>. 0704 * 0705 * Reimplemented from base class. 0706 * 0707 * Updates the widget (except for windows that do not 0708 * specify a <tt>focusPolicy()</tt>). 0709 * 0710 * @param event the <tt>QEvent::FocusIn</tt> to be handled. */ 0711 void MultiSpinBox::focusInEvent(QFocusEvent *event) 0712 { 0713 QAbstractSpinBox::focusInEvent(event); 0714 switch (event->reason()) { 0715 case Qt::ShortcutFocusReason: 0716 case Qt::TabFocusReason: 0717 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0); 0718 // Make sure that the buttons for step up and step down 0719 // are updated. 0720 update(); 0721 return; 0722 case Qt::BacktabFocusReason: 0723 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_sectionConfigurations.count() - 1); 0724 // Make sure that the buttons for step up and step down 0725 // are updated. 0726 update(); 0727 return; 0728 case Qt::MouseFocusReason: 0729 case Qt::ActiveWindowFocusReason: 0730 case Qt::PopupFocusReason: 0731 case Qt::MenuBarFocusReason: 0732 case Qt::OtherFocusReason: 0733 case Qt::NoFocusReason: 0734 default: 0735 update(); 0736 return; 0737 } 0738 } 0739 0740 /** @brief Increase or decrease the current section’s value. 0741 * 0742 * Reimplemented from base class. 0743 * 0744 * As of the base class’s documentation: 0745 * 0746 * > Virtual function that is called whenever the user triggers a step. 0747 * > For example, pressing <tt>Qt::Key_Down</tt> will trigger a call 0748 * > to <tt>stepBy(-1)</tt>, whereas pressing <tt>Qt::Key_PageUp</tt> will 0749 * > trigger a call to <tt>stepBy(10)</tt>. 0750 * 0751 * @param steps Number of steps to be taken. The step size is 0752 * the @ref MultiSpinBoxSection::singleStep of the current section. */ 0753 void MultiSpinBox::stepBy(int steps) 0754 { 0755 const QListSizeType currentIndex = d_pointer->m_currentIndex; 0756 QList<double> myValues = sectionValues(); 0757 myValues[currentIndex] += steps * d_pointer->m_sectionConfigurations.at(currentIndex).singleStep(); 0758 // As explained in QAbstractSpinBox documentation: 0759 // “Note that this function is called even if the resulting value will 0760 // be outside the bounds of minimum and maximum. It’s this function’s 0761 // job to handle these situations.” 0762 // Therefore, the result has to be bound to the actual minimum and maximum 0763 // values. 0764 setSectionValues(myValues); 0765 // Update the content of the QLineEdit and select the current 0766 // value (as cursor text selection): 0767 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(currentIndex); 0768 update(); // Make sure the buttons for step-up and step-down are updated. 0769 } 0770 0771 /** @brief Updates the value of the current section. 0772 * 0773 * This slot is meant to be connected to the 0774 * <tt>&QLineEdit::textChanged()</tt> signal of 0775 * the <tt>MultiSpinBox::lineEdit()</tt> child widget. 0776 * , 0777 * @param lineEditText The text of the <tt>lineEdit()</tt>. The value 0778 * will be updated according to this parameter. Only changes in 0779 * the <em>current</em> section’s value are expected, no changes in 0780 * other sections. (If this parameter has an invalid value, a warning will 0781 * be printed to stderr and the function returns without further action.) */ 0782 void MultiSpinBoxPrivate::updateCurrentValueFromText(const QString &lineEditText) 0783 { 0784 // Get the clean test. That means, we start with “text”, but 0785 // we remove the m_currentSectionTextBeforeValue and the 0786 // m_currentSectionTextAfterValue, so that only the text of 0787 // the value itself remains. 0788 QString cleanText = lineEditText; 0789 if (cleanText.startsWith(m_textBeforeCurrentValue)) { 0790 cleanText.remove(0, m_textBeforeCurrentValue.count()); 0791 } else { 0792 // The text does not start with the correct characters. 0793 // This is an error. 0794 qWarning() << "The function" << __func__ // 0795 << "in file" << __FILE__ // 0796 << "near to line" << __LINE__ // 0797 << "was called with the invalid “lineEditText“ argument “" << lineEditText // 0798 << "” that does not start with the expected character sequence “" << m_textBeforeCurrentValue << ". " // 0799 << "The call is ignored. This is a bug."; 0800 return; 0801 } 0802 if (cleanText.endsWith(m_textAfterCurrentValue)) { 0803 cleanText.chop(m_textAfterCurrentValue.count()); 0804 } else { 0805 // The text does not start with the correct characters. 0806 // This is an error. 0807 qWarning() << "The function" << __func__ // 0808 << "in file" << __FILE__ // 0809 << "near to line" << __LINE__ // 0810 << "was called with the invalid “lineEditText“ argument “" << lineEditText // 0811 << "” that does not end with the expected character sequence “" << m_textAfterCurrentValue << ". " // 0812 << "The call is ignored. This is a bug."; 0813 return; 0814 } 0815 0816 // Update… 0817 bool ok; 0818 QList<double> myValues = q_pointer->sectionValues(); 0819 myValues[m_currentIndex] = q_pointer->locale().toDouble(cleanText, &ok); 0820 setSectionValuesWithoutFurtherUpdating(myValues); 0821 // Make sure that the buttons for step up and step down are updated. 0822 q_pointer->update(); 0823 // The lineEdit()->text() property is intentionally not updated because 0824 // this function is meant to receive signals of the very same lineEdit(). 0825 } 0826 0827 /** @brief The main event handler. 0828 * 0829 * Reimplemented from base class. 0830 * 0831 * On <tt>QEvent::Type::LocaleChange</tt> it updates the spinbox content 0832 * accordingly. Apart from that, it calls the implementation in the parent 0833 * class. 0834 * 0835 * @returns The base class’s return value. 0836 * 0837 * @param event the event to be handled. */ 0838 bool MultiSpinBox::event(QEvent *event) 0839 { 0840 if (event->type() == QEvent::Type::LocaleChange) { 0841 d_pointer->updatePrefixValueSuffixText(); 0842 d_pointer->m_validator->setPrefix(d_pointer->m_textBeforeCurrentValue); 0843 d_pointer->m_validator->setSuffix(d_pointer->m_textAfterCurrentValue); 0844 d_pointer->m_validator->setRange( 0845 // Minimum 0846 d_pointer->m_sectionConfigurations.at(d_pointer->m_currentIndex).minimum(), 0847 // Maximum 0848 d_pointer->m_sectionConfigurations.at(d_pointer->m_currentIndex).maximum()); 0849 lineEdit()->setText(d_pointer->m_textBeforeCurrentValue + d_pointer->m_textOfCurrentValue + d_pointer->m_textAfterCurrentValue); 0850 } 0851 return QAbstractSpinBox::event(event); 0852 } 0853 0854 /** @brief Updates the widget according to the new cursor position. 0855 * 0856 * This slot is meant to be connected to the 0857 * <tt>QLineEdit::cursorPositionChanged()</tt> signal of 0858 * the <tt>MultiSpinBox::lineEdit()</tt> child widget. 0859 * 0860 * @param oldPos the old cursor position (previous position) 0861 * @param newPos the new cursor position (current position) */ 0862 void MultiSpinBoxPrivate::reactOnCursorPositionChange(const int oldPos, const int newPos) 0863 { 0864 Q_UNUSED(oldPos) 0865 0866 // We are working here with QString::length() and 0867 // QLineEdit::cursorPosition(). Both are of type “int”, and both are 0868 // measured in UTF-16 code units. While it feels quite uncomfortable 0869 // to measure cursor positions in code _units_ and not at least in 0870 // in code _points_, it does not matter for this code, as the behaviour 0871 // is consistent between both usages. 0872 0873 if (isCursorTouchingCurrentSectionValue()) { 0874 // We are within the value text of our current section value. 0875 // There is nothing to do here. 0876 return; 0877 } 0878 0879 QSignalBlocker myBlocker(q_pointer->lineEdit()); 0880 0881 // The new position is not at the current value, but the old one might 0882 // have been. So maybe we have to correct the value, which might change 0883 // its length. If the new cursor position is after this value, it will 0884 // have to be adapted (if the value had been changed or alternated). 0885 const QListSizeType oldTextLength = q_pointer->lineEdit()->text().length(); 0886 const bool mustAdjustCursorPosition = // 0887 (newPos > (oldTextLength - m_textAfterCurrentValue.length())); 0888 0889 // Calculate in which section the cursor is 0890 int sectionOfTheNewCursorPosition; 0891 QStringLength reference = 0; 0892 for (sectionOfTheNewCursorPosition = 0; // 0893 sectionOfTheNewCursorPosition < m_sectionConfigurations.count() - 1; // 0894 ++sectionOfTheNewCursorPosition // 0895 ) { 0896 reference += m_sectionConfigurations // 0897 .at(sectionOfTheNewCursorPosition) // 0898 .prefix() // 0899 .length(); 0900 reference += formattedValue(sectionOfTheNewCursorPosition).length(); 0901 reference += m_sectionConfigurations // 0902 .at(sectionOfTheNewCursorPosition) // 0903 .suffix() // 0904 .length(); 0905 if (newPos <= reference) { 0906 break; 0907 } 0908 } 0909 0910 updatePrefixValueSuffixText(); 0911 setCurrentIndexWithoutUpdatingText(sectionOfTheNewCursorPosition); 0912 q_pointer->lineEdit()->setText(m_textBeforeCurrentValue // 0913 + m_textOfCurrentValue // 0914 + m_textAfterCurrentValue); 0915 int correctedCursorPosition = newPos; 0916 if (mustAdjustCursorPosition) { 0917 correctedCursorPosition = // 0918 static_cast<int>(newPos // 0919 + q_pointer->lineEdit()->text().length() // 0920 - oldTextLength); 0921 } 0922 q_pointer->lineEdit()->setCursorPosition(correctedCursorPosition); 0923 0924 // Make sure that the buttons for step up and step down are updated. 0925 q_pointer->update(); 0926 } 0927 0928 /** @brief Constructor 0929 * 0930 * @param w The widget to which the newly created object will correspond. */ 0931 AccessibleMultiSpinBox::AccessibleMultiSpinBox(MultiSpinBox *w) 0932 : QAccessibleWidget(w, QAccessible::Role::SpinBox) 0933 { 0934 } 0935 0936 /** @brief Destructor */ 0937 AccessibleMultiSpinBox::~AccessibleMultiSpinBox() 0938 { 0939 } 0940 0941 /** @brief Factory function. 0942 * 0943 * This signature of this function is exactly as defined by 0944 * <tt>QAccessible::InterfaceFactory</tt>. A pointer to this function 0945 * can therefore be passed to <tt>QAccessible::installFactory()</tt>. 0946 * 0947 * @param classname The class name for which an interface is requested 0948 * @param object The object for which an interface is requested 0949 * 0950 * @returns If this class corresponds to the request, it returns an object 0951 * of this class. Otherwise, a null-pointer will be returned. */ 0952 QAccessibleInterface *AccessibleMultiSpinBox::factory(const QString &classname, QObject *object) 0953 { 0954 QAccessibleInterface *interface = nullptr; 0955 const QString multiSpinBoxClassName = QString::fromUtf8( 0956 // className() returns const char *. Its encoding is not documented. 0957 // Hopefully, as we use UTF8 in this library as “input character set” 0958 // and also as “Narrow execution character set”, the encoding 0959 // might be also UTF8… 0960 MultiSpinBox::staticMetaObject.className()); 0961 MultiSpinBox *myMultiSpinBox = qobject_cast<MultiSpinBox *>(object); 0962 if ((classname == multiSpinBoxClassName) && myMultiSpinBox) { 0963 interface = new AccessibleMultiSpinBox(myMultiSpinBox); 0964 } 0965 return interface; 0966 } 0967 0968 /** @brief Current implementation does nothing. 0969 * 0970 * Reimplemented from base class. 0971 * 0972 * @internal 0973 * 0974 * This class has to be necessarily reimplemented because the base 0975 * class’s implementation is incompatible with <em>this</em> class 0976 * and could produce undefined behaviour. 0977 * 0978 * If this function would be reimplemented in the future, here 0979 * is the specification: 0980 * 0981 * @note Qt’s own child classes use this function to implement <tt>Ctrl-U</tt>. 0982 * But this not relevant here, because this class has its own implementation 0983 * for keyboard event handling (and currently does not even handle 0984 * <tt>Ctrl-U</tt> at all). 0985 * 0986 * <tt>brief</tt> Clears the value of the current section. 0987 * 0988 * The other sections and also the prefix and suffix of the current 0989 * section stay visible. 0990 * 0991 * The base class is documented as: 0992 * <em>Clears the lineedit of all text but prefix and suffix.</em> The 0993 * semantic of this reimplementation is slightly different; it is however 0994 * the same semantic that also QDateTimeEdit, another child class 0995 * of <tt>QAbstractSpinBox</tt>, applies. */ 0996 void MultiSpinBox::clear() 0997 { 0998 } 0999 1000 } // namespace PerceptualColor