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 #ifndef MULTISPINBOX_H
0005 #define MULTISPINBOX_H
0006 
0007 #include "constpropagatinguniquepointer.h"
0008 #include "importexport.h"
0009 #include <qabstractspinbox.h>
0010 #include <qglobal.h>
0011 #include <qlineedit.h>
0012 #include <qlist.h>
0013 #include <qsize.h>
0014 class QAction;
0015 class QEvent;
0016 class QFocusEvent;
0017 class QWidget;
0018 
0019 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0020 // Including multispinboxsection.h is necessary on Qt6,
0021 // otherwise moc will fail. (IWYU does not detect this dependency.)
0022 #include "multispinboxsection.h" // IWYU pragma: keep
0023 #include <qtmetamacros.h>
0024 #else
0025 #include <qobjectdefs.h>
0026 #include <qstring.h>
0027 namespace PerceptualColor
0028 {
0029 class MultiSpinBoxSection;
0030 }
0031 class QObject;
0032 #endif
0033 
0034 namespace PerceptualColor
0035 {
0036 class MultiSpinBoxPrivate;
0037 
0038 /** @brief A spin box that can hold multiple sections (each with its own
0039  * value) at the same time.
0040  *
0041  * This widget is similar to <tt>QDateTimeEdit</tt> which also provides
0042  * multiple sections (day, month, year…) within a single spin box.
0043  * However, <em>this</em> widget is flexible. You can define on your own
0044  * the behaviour of each section.
0045  *
0046  * @image html MultiSpinBox.png "MultiSpinBox" width=200
0047  *
0048  * This widget works with floating point precision. You can set
0049  * the number of decimal places for each section individually,
0050  * via @ref MultiSpinBoxSection::decimals. (This
0051  * value can also be <tt>0</tt> to get integer-like behaviour.)
0052  *
0053  * Example code to create a @ref MultiSpinBox for a HSV color value
0054  * (Hue 0°–360°, Saturation 0–255, Value 0–255) comes here:
0055  * @snippet testmultispinbox.cpp MultiSpinBox Basic example
0056  *
0057  * You can also have additional buttons within the spin box via the
0058  * @ref addActionButton() function.
0059  *
0060  * @note This class inherits from <tt>QAbstractSpinBox</tt>, but some
0061  * parts of the parent class’s API are not supported in <em>this</em>
0062  * class. Do not use them:
0063  * - <tt>selectAll()</tt> does not work as expected.
0064  * - <tt>wrapping()</tt> is ignored. Instead, you can configures
0065  *   the <em>wrapping</em> individually for each section via
0066  *   @ref MultiSpinBoxSection::isWrapping.
0067  * - <tt>specialValue()</tt> is not supported.
0068  *   <!-- Just as in QDateTimeEdit! -->
0069  * - <tt>hasAcceptableInput()</tt> is not guaranteed to obey to a particular
0070  *   and stable semantic.
0071  * - <tt>fixup()</tt>, <tt>interpretText()</tt>, <tt>validate()</tt> are
0072  *   not used nor do they anything.
0073  * - <tt>keyboardTracking()</tt> is ignored. See the signal
0074  *   @ref sectionValuesChanged for details.
0075  * - <tt>correctionMode()</tt> is ignored.
0076  * - <tt>isGroupSeparatorShown</tt> is ignored.
0077  *
0078  * @internal
0079  *
0080  * Further remarks on inherited API of <tt>QAbstractSpinBox</tt>:
0081  * - <tt>selectAll()</tt>:
0082  *   This slot has a default behaviour that relies on internal
0083  *   <tt>QAbstractSpinBox</tt> private implementations, which we cannot use
0084  *   because they are not part of the public API and can therefore change
0085  *   at any moment. As it isn’t virtual, we cannot reimplement it either.
0086  * - <tt>fixup(), interpretText(), validate()</tt>:
0087  *   As long as we do not  interact with the private API of
0088  *   <tt>QAbstractSpinBox</tt> (which we  cannot do because
0089  *   there is no stability guaranteed), those functions  are never
0090  *   called by <tt>QAbstractSpinBox</tt> nor does their default
0091  *   implementation do anything. (They seem rather like an implementation
0092  *   detail of Qt that was leaked to the public API.) We don’t use them
0093  *   either.
0094  * - <tt>isGroupSeparatorShown</tt>:
0095  *   Implementing this seems complicate. In the base class, the setter
0096  *   is  not virtual, and this property does not have a notify signal
0097  *   either.  But we would have to react on a changes in this property:
0098  *   The content of the <tt>QLineEdit</tt> has to be updated. And the
0099  *   @ref minimumSizeHint and the @ref sizeHint will change, therefore
0100  *   <tt>updateGeometry</tt> has to be called. It seems better not to
0101  *   implement this. Alternatively, it could be implemented with a
0102  *   per-section approach via  @ref MultiSpinBoxSection.
0103  *
0104  * @note The interface of this class could theoretically
0105  * be similar to other Qt classes that offer similar concepts of various
0106  * data within a list: QComboBox, QHeaderView, QDateTimeEdit, QList – of
0107  * course with consistent naming. But usually you will not modify a single
0108  * section configuration, but the hole set of configurations. Therefore we do
0109  * the configuration by @ref MultiSpinBoxSection objects, similar
0110  * to <tt>QNetworkConfiguration</tt> objects. Allowing changes to individual
0111  * sections would require a lot of additional code to make sure that after
0112  * such a change, the text cursor is set the the appropriate position and
0113  * the text selection is also appropriate. This might be problematic,
0114  * and gives also little benefit.
0115  * However, a full-featured interface could look like that:
0116  * @snippet testmultispinbox.cpp MultiSpinBox Full-featured interface
0117  *
0118  * @todo i18n bug: Use a MultiSpinBox with a locale that uses “,” as decimal
0119  * separator, and with a value with some decimals. Try to type “0,1”. It will
0120  * not be accepted. However, “0.1” will be accepted (and, when moving on,
0121  * corrected to “0,1”). This is not the expected behaviour.
0122  *
0123  * @todo i18n bug: Enter HLC values like “<tt>80.</tt>” or “<tt>80,</tt>”
0124  * or “<tt>80e</tt>”. Depending on the locale, it is possible to
0125  * actually enter these characters, but apparently on validation it
0126  * is not accepted and the value is replaced by <tt>0</tt>.
0127  * MultiSpinBox should never become 0 because the validator
0128  * allows something that the converter cannot convert!
0129  *
0130  * @todo In @ref ColorDialog go to the HLC @ref MultiSpinBox and place
0131  * the text cursor behind the degree sign, than press the ⌫ (backspace) key.
0132  * Actual behaviour: An error message is printed on the console: “The function
0133  * updateCurrentValueFromText in file […]multispinbox.cpp near […] was called
0134  * with the invalid “lineEditText“ argument […].  The call is ignored.
0135  * This is a bug.” Expected behaviour: No error message is printed.
0136  *
0137  * @todo <tt>Ctrl-A</tt> support for this class. (Does this shortcut
0138  * trigger <tt>selectAll()</tt>?) <tt>Ctrl-U</tt> support for this class?
0139  * If so, do it via @ref clear(). And: If the user tries to delete
0140  * everything, delete instead only the current value!? (By the way:
0141  * How does QDateTimeEdit handle this?)
0142  *
0143  * @todo Bug: In @ref ColorDialog, choose a tab with one of the diagrams.
0144  * Then, switch back the the “numeric“ tab. Expected behaviour: When
0145  * a @ref MultiSpinBox gets back the focus, always the first section should
0146  * be <em>highlighted/selected</em>, independent from what was selected or
0147  * the cursor position before the @ref MultiSpinBox lost the focus.
0148  * (While <tt>QSpinBox</tt> and <tt>QDoubleSpinBox</tt> don’t do that
0149  * either, <tt>QDateTimeEdit</tt> indeed <em>does</em>, and that seems
0150  * appropriate also for @ref MultiSpinBox.
0151  *
0152  * @todo Now, @ref setSectionValues does not select automatically the first
0153  * section anymore. Is this in conformance with <tt>QDateTimeEdit</tt>?
0154  * Test: Change the value in the middle. Push “Apply” button. Now, the
0155  * curser is at the end of the spin box, but the active section is still
0156  * the one in the middle (you can try this by using your mouse wheel on
0157  * the widget).
0158  *
0159  * @todo Currently, if the widget has <em>not</em> the focus but the
0160  * mouse moves over it and the scroll wheel is used, it’s the first
0161  * section that will be changed, and not the one where the mouse is,
0162  * as the user might expect. Even QDateTimeEdit does the same thing
0163  * (thus they do not change the first section, but the last one that
0164  * was editing before). But it would be great if we could do better here.
0165  * But: Is this realistic and will the required code work on all
0166  * platforms?
0167  *
0168  * @todo When adding Bengali digits (for example by copy and paste) to a
0169  * @ref MultiSpinBox that was localized to en_US, than sometimes this is
0170  * accepted (thought later “corrected” to 0), and sometimes not. This
0171  * behaviour is inconsistent and wrong.
0172  *
0173  * @todo Apparently, the validator doesn’t restrict the input actually to the
0174  * given range. For QDoubleSpinBox however, the line edit <em>is</em>
0175  * restricted! Example: even if 100 is maximum, it is possible to write 444.
0176  * Maybe our @ref ExtendedDoubleValidator should not rely on Qt’s validator,
0177  * but on if QLocale is able to convert (result: valid) or not (result:
0178  * invalid)?!.
0179  *
0180  * @todo If exposing this class as public API of this library, would
0181  * it make sense to implement the complete public API of QAbstractSpinBox
0182  * from which we inherit? Currently, some parts of the QAbstractSpinBox API
0183  * are nor (properly) implemented by this class… */
0184 class PERCEPTUALCOLOR_IMPORTEXPORT MultiSpinBox : public QAbstractSpinBox
0185 {
0186     Q_OBJECT
0187 
0188     /** @brief A list containing the values of all sections.
0189      *
0190      * @note It is not this property, but @ref sectionConfigurations
0191      * which determines the actually available count of sections in this
0192      * widget. If you want to change the number of available sections,
0193      * call <em>first</em> @ref setSectionConfigurations and only
0194      * <em>after</em> that adapt this property.
0195      *
0196      * @invariant This property contains always as many elements as
0197      * @ref sectionConfigurations contains.
0198      *
0199      * @sa READ @ref sectionValues() const
0200      * @sa WRITE @ref setSectionValues()
0201      * @sa NOTIFY @ref sectionValuesChanged() */
0202     Q_PROPERTY(QList<double> sectionValues READ sectionValues WRITE setSectionValues NOTIFY sectionValuesChanged USER true)
0203 
0204 public:
0205     Q_INVOKABLE explicit MultiSpinBox(QWidget *parent = nullptr);
0206     /** @brief Default destructor */
0207     virtual ~MultiSpinBox() noexcept override;
0208     void addActionButton(QAction *action, QLineEdit::ActionPosition position);
0209     virtual void clear() override;
0210     [[nodiscard]] virtual QSize minimumSizeHint() const override;
0211     [[nodiscard]] Q_INVOKABLE QList<PerceptualColor::MultiSpinBoxSection> sectionConfigurations() const;
0212     /** @brief Getter for property @ref sectionValues
0213      *  @returns the property @ref sectionValues */
0214     [[nodiscard]] QList<double> sectionValues() const;
0215     Q_INVOKABLE void setSectionConfigurations(const QList<PerceptualColor::MultiSpinBoxSection> &newSectionConfigurations);
0216     [[nodiscard]] virtual QSize sizeHint() const override;
0217     virtual void stepBy(int steps) override;
0218 
0219 public Q_SLOTS:
0220     void setSectionValues(const QList<double> &newSectionValues);
0221 
0222 Q_SIGNALS:
0223     /** @brief Notify signal for property @ref sectionValues.
0224      *
0225      * This signal is emitted whenever the value in one or more sections
0226      * changes.
0227      *
0228      * @param newSectionValues the new @ref sectionValues
0229      *
0230      * Depending on your use case (for
0231      * example if you want to use for <em>queued</em> signal-slot connections),
0232      * you might consider calling <tt>qRegisterMetaType()</tt> for
0233      * this type, once you have a QApplication object.
0234      *
0235      * @note The property <tt>keyboardTracking()</tt> of the base class
0236      * is currently ignored. Keyboard tracking is <em>always</em> enabled:
0237      * The spinbox emits this signal while the new value is being entered
0238      * from the keyboard – one signal for each key stroke. */
0239     void sectionValuesChanged(const QList<double> &newSectionValues);
0240 
0241 protected:
0242     virtual void changeEvent(QEvent *event) override;
0243     virtual bool event(QEvent *event) override;
0244     virtual void focusInEvent(QFocusEvent *event) override;
0245     virtual bool focusNextPrevChild(bool next) override;
0246     virtual void focusOutEvent(QFocusEvent *event) override;
0247     [[nodiscard]] virtual QAbstractSpinBox::StepEnabled stepEnabled() const override;
0248 
0249 private:
0250     Q_DISABLE_COPY(MultiSpinBox)
0251 
0252     /** @internal
0253      *
0254      * @brief Declare the private implementation as friend class.
0255      *
0256      * This allows the private class to access the protected members and
0257      * functions of instances of <em>this</em> class. */
0258     friend class MultiSpinBoxPrivate;
0259     /** @brief Pointer to implementation (pimpl) */
0260     ConstPropagatingUniquePointer<MultiSpinBoxPrivate> d_pointer;
0261 
0262     /** @internal @brief Only for unit tests. */
0263     friend class TestMultiSpinBox;
0264 };
0265 
0266 } // namespace PerceptualColor
0267 
0268 #endif // MULTISPINBOX_H