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