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