Perceptual Color

multispinbox.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own headers
5// First the interface, which forces the header to be self-contained.
6#include "multispinbox.h"
7// Second, the private implementation.
8#include "multispinbox_p.h" // IWYU pragma: associated
9
10#include "constpropagatingrawpointer.h"
11#include "constpropagatinguniquepointer.h"
12#include "extendeddoublevalidator.h"
13#include "helpermath.h"
14#include "multispinboxsection.h"
15#include <math.h>
16#include <qaccessible.h>
17#include <qaccessiblewidget.h>
18#include <qcoreevent.h>
19#include <qdebug.h>
20#include <qevent.h>
21#include <qfontmetrics.h>
22#include <qglobal.h>
23#include <qlineedit.h>
24#include <qlocale.h>
25#include <qnamespace.h>
26#include <qobject.h>
27#include <qpointer.h>
28#include <qstringbuilder.h>
29#include <qstringliteral.h>
30#include <qstyle.h>
31#include <qstyleoption.h>
32#include <qwidget.h>
33class QAction;
34
35#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
36#include <qobjectdefs.h>
37#else
38#endif
39
40namespace PerceptualColor
41{
42/** @brief If the text cursor is touching at the current section’s value.
43 *
44 * Everything from the cursor position exactly before the value itself up
45 * to the cursor position exactly after the value itself. Prefixes and
46 * suffixes are not considered as part of the value. Example: “ab12cd”
47 * (prefix “ab”, value 12, suffix “cd”). The cursor positions 2, 3 and 4 are
48 * considered <em>touching</em> the current value.
49 *
50 * @returns <tt>true</tt> if the text cursor is touching at the current
51 * section’s value.. <tt>false</tt> otherwise. */
52bool MultiSpinBoxPrivate::isCursorTouchingCurrentSectionValue() const
53{
54 const auto cursorPosition = q_pointer->lineEdit()->cursorPosition();
55 const bool highEnough = (cursorPosition >= m_textBeforeCurrentValue.length());
56 const auto after = q_pointer->lineEdit()->text().length() //
57 - m_textAfterCurrentValue.length();
58 const bool lowEnough = (cursorPosition <= after);
59 return (highEnough && lowEnough);
60}
61
62/** @brief The recommended minimum size for the widget
63 *
64 * Reimplemented from base class.
65 *
66 * @returns the recommended minimum size for the widget
67 *
68 * @internal
69 *
70 * @sa @ref sizeHint()
71 *
72 * @note The minimum size of the widget is the same as @ref sizeHint(). This
73 * behaviour is different from <tt>QSpinBox</tt> and <tt>QDoubleSpinBox</tt>
74 * that have a minimum size hint that allows for displaying only prefix and
75 * value, but not the suffix. However, such a behavior does not seem
76 * appropriate for a @ref MultiSpinBox because it could be confusing, given
77 * that its content is more complex. */
79{
80 return sizeHint();
81}
82
83/** @brief Constructor
84 *
85 * @param parent the parent widget, if any */
87 : QAbstractSpinBox(parent)
88 , d_pointer(new MultiSpinBoxPrivate(this))
89{
90 // Set up the m_validator
91 d_pointer->m_validator = new ExtendedDoubleValidator(this);
92 d_pointer->m_validator->setLocale(locale());
93 lineEdit()->setValidator(d_pointer->m_validator);
94
95 // Initialize the configuration (default: only one section).
96 // This will also change section values to exactly one element.
98 setSectionValues(QList<double>{MultiSpinBoxPrivate::defaultSectionValue});
99 d_pointer->m_currentIndex = -1; // This will force
100 // setCurrentIndexAndUpdateTextAndSelectValue()
101 // to really apply the changes, including updating
102 // the validator:
103 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0);
104
105 // Connect signals and slots
106 connect(lineEdit(), // sender
107 &QLineEdit::textChanged, // signal
108 d_pointer.get(), // receiver
109 &MultiSpinBoxPrivate::updateCurrentValueFromText // slot
110 );
111 connect(lineEdit(), // sender
113 d_pointer.get(), // receiver
114 &MultiSpinBoxPrivate::reactOnCursorPositionChange // slot
115 );
116 connect(this, // sender
118 d_pointer.get(), // receiver
119 &MultiSpinBoxPrivate::setCurrentIndexToZeroAndUpdateTextAndSelectValue // slot
120 );
121
122 // Initialize accessibility support
124 // It’s safe to call installFactory() multiple times with the
125 // same factory. If the factory is yet installed, it will not
126 // be installed again.
127 &AccessibleMultiSpinBox::factory);
128}
129
130/** @brief Default destructor */
132{
133}
134
135/** @brief Constructor
136 *
137 * @param backLink Pointer to the object from which <em>this</em> object
138 * is the private implementation. */
139MultiSpinBoxPrivate::MultiSpinBoxPrivate(MultiSpinBox *backLink)
140 : q_pointer(backLink)
141{
142}
143
144/** @brief The recommended size for the widget
145 *
146 * Reimplemented from base class.
147 *
148 * @returns the size hint
149 *
150 * @internal
151 *
152 * @note Some widget styles like CDE and Motif calculate badly (too small)
153 * the size for QAbstractSpinBox and its child classes, and therefore also
154 * for this widget.
155 *
156 * @sa @ref minimumSizeHint() */
158{
159 // This function intentionally does not cache the text string.
160 // Which variant is the longest text string, that depends on the current
161 // font policy. This might have changed since the last call. Therefore,
162 // each time this function is called, we calculate again the longest
163 // test string (“completeString”).
164
166
167 const QFontMetrics myFontMetrics(fontMetrics());
168 QList<MultiSpinBoxSection> myConfiguration = d_pointer->m_sectionConfigurations;
169 const int height = lineEdit()->sizeHint().height();
170 int width = 0;
171 QString completeString;
172
173 // Get the text for all the sections
174 for (int i = 0; i < myConfiguration.count(); ++i) {
175 // Prefix
176 completeString += myConfiguration.at(i).prefix();
177 // For each section, test if the minimum value or the maximum
178 // takes more space (width). Choose the one that takes more place
179 // (width).
180 const QString textOfMinimumValue = locale().toString( //
181 myConfiguration.at(i).minimum(), // value
182 'f', // format
183 myConfiguration.at(i).decimals() // precision
184 );
185 const QString textOfMaximumValue = locale().toString( //
186 myConfiguration.at(i).maximum(), // value
187 'f', // format
188 myConfiguration.at(i).decimals() // precision
189 );
190 if (myFontMetrics.horizontalAdvance(textOfMinimumValue) > myFontMetrics.horizontalAdvance(textOfMaximumValue)) {
191 completeString += textOfMinimumValue;
192 } else {
193 completeString += textOfMaximumValue;
194 }
195 // Suffix
196 completeString += myConfiguration.at(i).suffix();
197 }
198
199 // Add some extra space, just as QSpinBox seems to do also.
200 completeString += QStringLiteral(u" ");
201
202 // Calculate string width and add two extra pixel for cursor
203 // blinking space.
204 width = myFontMetrics.horizontalAdvance(completeString) + 2;
205
206 // Calculate the final size in pixel
207 QStyleOptionSpinBox myStyleOptionsForSpinBoxes;
208 initStyleOption(&myStyleOptionsForSpinBoxes);
209 myStyleOptionsForSpinBoxes.buttonSymbols = QAbstractSpinBox::PlusMinus;
210
211 const QSize contentSize(width, height);
212 // Calculate widget size necessary to display a given content
213 QSize result = style()->sizeFromContents(
214 // In the Kvantum style in version 0.18, there was a bug that returned
215 // via QStyle::sizeFromContents() a width that was too small. In
216 // Kvantum version 1.0.1 this is fixed.
217 QStyle::CT_SpinBox, // type
218 &myStyleOptionsForSpinBoxes, // style options
219 contentSize, // size of the content
220 this // optional widget argument (for better calculations)
221 );
222
223 if (d_pointer->m_actionButtonCount > 0) {
224 // Determine the size of icons for actions similar to what Qt
225 // does in QLineEditPrivate::sideWidgetParameters() and than
226 // add this to the size hint.
227 const int actionButtonIconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, // pixel metric type
228 nullptr, // style options
229 lineEdit() // widget (optional)
230 );
231 const int actionButtonMargin = actionButtonIconSize / 4;
232 const int actionButtonWidth = actionButtonIconSize + 6;
233 // Only 1 margin per button:
234 const int actionButtonSpace = actionButtonWidth + actionButtonMargin;
235 result.setWidth(result.width() + d_pointer->m_actionButtonCount * actionButtonSpace);
236 }
237
238 return result;
239}
240
241/** @brief Handle state changes
242 *
243 * Implements reaction on <tt>QEvent::LanguageChange</tt>.
244 *
245 * Reimplemented from base class.
246 *
247 * @param event The event to process */
249{
250 // QEvent::StyleChange or QEvent::FontChange are not handled here
251 // because they trigger yet a content and geometry update in the
252 // base class’s implementation of this function.
253 if ( //
254 (event->type() == QEvent::LanguageChange) //
255 || (event->type() == QEvent::LocaleChange) //
256 // The base class’s implementation for QEvent::LayoutDirectionChange
257 // would only call update, not updateGeometry…
258 || (event->type() == QEvent::LayoutDirectionChange) //
259 ) {
260 // Updates the widget content and its geometry
261 update();
263 }
265}
266
267/** @brief Adds to the widget a button associated with the given action.
268 *
269 * The icon of the action will be displayed as button. If the action has
270 * no icon, just an empty space will be displayed.
271 *
272 * @image html MultiSpinBoxWithButton.png "MultiSpinBox with action button" width=200
273 *
274 * It is possible to add more than one action.
275 *
276 * @param action This action that will be executed when clicking the button.
277 * (The ownership of the action object remains unchanged.)
278 * @param position The position of the button within the widget (left
279 * or right)
280 * @note See @ref hidpisupport "High DPI support" about how to enable
281 * support for high-DPI icons.
282 * @note The action will <em>not</em> appear in the
283 * <tt>QWidget::actions()</tt> function of this class. */
285{
286 lineEdit()->addAction(action, position);
287 d_pointer->m_actionButtonCount += 1;
288 // The size hints have changed, because an additional button needs
289 // more space.
291}
292
293/** @brief Get formatted value for a given section.
294 * @param index The index of the section
295 * @returns The value of the given section, formatted (without prefix or
296 * suffix), as text. */
297QString MultiSpinBoxPrivate::formattedValue(QListSizeType index) const
298{
299 return q_pointer->locale().toString(
300 // The value to be formatted:
301 q_pointer->sectionValues().at(index),
302 // Format as floating point with decimal digits
303 'f',
304 // Number of decimal digits
305 m_sectionConfigurations.at(index).decimals());
306}
307
308/** @brief Updates prefix, value and suffix text
309 *
310 * @pre <tt>0 <= @ref m_currentIndex < @ref m_sectionConfigurations .count()</tt>
311 *
312 * @post Updates @ref m_textBeforeCurrentValue, @ref m_textOfCurrentValue,
313 * @ref m_textAfterCurrentValue to the correct values based
314 * on @ref m_currentIndex. */
315void MultiSpinBoxPrivate::updatePrefixValueSuffixText()
316{
317 QListSizeType i;
318
319 // Update m_currentSectionTextBeforeValue
320 m_textBeforeCurrentValue = QString();
321 for (i = 0; i < m_currentIndex; ++i) {
322 m_textBeforeCurrentValue.append(m_sectionConfigurations.at(i).prefix());
323 m_textBeforeCurrentValue.append(formattedValue(i));
324 m_textBeforeCurrentValue.append(m_sectionConfigurations.at(i).suffix());
325 }
326 m_textBeforeCurrentValue.append(m_sectionConfigurations.at(m_currentIndex).prefix());
327
328 // Update m_currentSectionTextOfTheValue
329 m_textOfCurrentValue = formattedValue(m_currentIndex);
330
331 // Update m_currentSectionTextAfterValue
332 m_textAfterCurrentValue = QString();
333 m_textAfterCurrentValue.append(m_sectionConfigurations.at(m_currentIndex).suffix());
334 for (i = m_currentIndex + 1; i < m_sectionConfigurations.count(); ++i) {
335 m_textAfterCurrentValue.append(m_sectionConfigurations.at(i).prefix());
336
337 m_textAfterCurrentValue.append(formattedValue(i));
338 m_textAfterCurrentValue.append(m_sectionConfigurations.at(i).suffix());
339 }
340}
341
342/** @brief Sets the current section index to <tt>0</tt>.
343 *
344 * Convenience function that simply calls
345 * @ref setCurrentIndexAndUpdateTextAndSelectValue with the
346 * argument <tt>0</tt>. */
347void MultiSpinBoxPrivate::setCurrentIndexToZeroAndUpdateTextAndSelectValue()
348{
349 setCurrentIndexAndUpdateTextAndSelectValue(0);
350}
351
352/** @brief Sets the current section index.
353 *
354 * Updates the text in the QLineEdit of this widget. If the widget has focus,
355 * it also selects the value of the new current section.
356 *
357 * @param newIndex The index of the new current section. Must be a valid
358 * index. The update will be done even if this argument is identical to
359 * the @ref m_currentIndex.
360 *
361 * @sa @ref setCurrentIndexToZeroAndUpdateTextAndSelectValue
362 * @sa @ref setCurrentIndexWithoutUpdatingText */
363void MultiSpinBoxPrivate::setCurrentIndexAndUpdateTextAndSelectValue(QListSizeType newIndex)
364{
365 QSignalBlocker myBlocker(q_pointer->lineEdit());
366 setCurrentIndexWithoutUpdatingText(newIndex);
367 // Update the line edit widget
368 q_pointer->lineEdit()->setText(m_textBeforeCurrentValue //
369 + m_textOfCurrentValue //
370 + m_textAfterCurrentValue);
371 const int lengthOfTextBeforeCurrentValue = //
372 static_cast<int>(m_textBeforeCurrentValue.length());
373 const int lengthOfTextOfCurrentValue = //
374 static_cast<int>(m_textOfCurrentValue.length());
375 if (q_pointer->hasFocus()) {
376 q_pointer->lineEdit()->setSelection( //
377 lengthOfTextBeforeCurrentValue, //
378 lengthOfTextOfCurrentValue);
379 } else {
380 q_pointer->lineEdit()->setCursorPosition( //
381 lengthOfTextBeforeCurrentValue + lengthOfTextOfCurrentValue);
382 }
383 // Make sure that the buttons for step up and step down are updated.
384 q_pointer->update();
385}
386
387/** @brief Sets the current section index without updating
388 * the <tt>QLineEdit</tt>.
389 *
390 * Does not change neither the text nor the cursor in the <tt>QLineEdit</tt>.
391 *
392 * @param newIndex The index of the new current section. Must be a valid index.
393 *
394 * @sa @ref setCurrentIndexAndUpdateTextAndSelectValue */
395void MultiSpinBoxPrivate::setCurrentIndexWithoutUpdatingText(QListSizeType newIndex)
396{
397 if (!isInRange<qsizetype>(0, newIndex, m_sectionConfigurations.count() - 1)) {
398 qWarning() << "The function" << __func__ //
399 << "in file" << __FILE__ //
400 << "near to line" << __LINE__ //
401 << "was called with an invalid “newIndex“ argument of" << newIndex //
402 << "thought the valid range is currently [" << 0 << ", " << m_sectionConfigurations.count() - 1 << "]. This is a bug.";
403 throw 0;
404 }
405
406 if (newIndex == m_currentIndex) {
407 // There is nothing to do here.
408 return;
409 }
410
411 // Apply the changes
412 m_currentIndex = newIndex;
413 updatePrefixValueSuffixText();
414 m_validator->setPrefix(m_textBeforeCurrentValue);
415 m_validator->setSuffix(m_textAfterCurrentValue);
416 m_validator->setRange(
417 // Minimum:
418 m_sectionConfigurations.at(m_currentIndex).minimum(),
419 // Maximum:
420 m_sectionConfigurations.at(m_currentIndex).maximum());
421
422 // The state (enabled/disabled) of the buttons “Step up” and “Step down”
423 // has to be updated. To force this, update() is called manually here:
424 q_pointer->update();
425}
426
427/** @brief Virtual function that determines whether stepping up and down is
428 * legal at any given time.
429 *
430 * Reimplemented from base class.
431 *
432 * @returns whether stepping up and down is legal */
434{
435 const MultiSpinBoxSection currentSectionConfiguration = d_pointer->m_sectionConfigurations.at(d_pointer->m_currentIndex);
436 const double currentSectionValue = sectionValues().at(d_pointer->m_currentIndex);
437
438 // When wrapping is enabled, step up and step down are always possible.
439 if (currentSectionConfiguration.isWrapping()) {
441 }
442
443 // When wrapping is not enabled, we have to compare the value with
444 // maximum and minimum.
446 // Test is step up should be enabled…
447 if (currentSectionValue < currentSectionConfiguration.maximum()) {
448 result.setFlag(StepUpEnabled, true);
449 }
450
451 // Test is step down should be enabled…
452 if (currentSectionValue > currentSectionConfiguration.minimum()) {
453 result.setFlag(StepDownEnabled, true);
454 }
455 return result;
456}
457
458/** @brief Sets the configuration for the sections.
459 *
460 * The first section will be selected as current section.
461 *
462 * @param newSectionConfigurations Defines the new sections. The new section
463 * count in this widget is the section count given in this list. Each section
464 * should have valid values: <tt>@ref MultiSpinBoxSection.minimum ≤
465 * @ref MultiSpinBoxSection.maximum</tt>. If the @ref sectionValues are
466 * not valid within the new section configurations, they will be fixed.
467 *
468 * @sa @ref sectionConfigurations() */
470{
471 if (newSectionConfigurations.count() < 1) {
472 return;
473 }
474
475 // Make sure that m_currentIndex will not run out-of-bound.
476 d_pointer->m_currentIndex = qBound(0, d_pointer->m_currentIndex, newSectionConfigurations.count());
477
478 // Set new section configuration
479 d_pointer->m_sectionConfigurations = newSectionConfigurations;
480
481 // Make sure the value list has the correct length and the
482 // values are updated to the new configuration:
484
485 // As the configuration has changed, the text selection might be
486 // undefined. Define it:
487 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_currentIndex);
488
489 // Make sure that the buttons for step up and step down are updated.
490 update();
491
492 // Make sure that the geometry is updated: sizeHint() and minimumSizeHint()
493 // both depend on the section configuration!
495}
496
497/** @brief Returns the configuration of all sections.
498 *
499 * @returns the configuration of all sections.
500 *
501 * @sa @ref setSectionConfigurations() */
503{
504 return d_pointer->m_sectionConfigurations;
505}
506
507// No documentation here (documentation of properties
508// and its getters are in the header)
510{
511 return d_pointer->m_sectionValues;
512}
513
514/** @brief Sets @ref m_sectionValues without updating other things.
515 *
516 * Other data of this widget, including the <tt>QLineEdit</tt> text,
517 * stays unmodified.
518 *
519 * @param newSectionValues The new section values. This list must have
520 * exactly as many items as @ref MultiSpinBox::sectionConfigurations.
521 * If the new values are not within the boundaries defined in
522 * the @ref MultiSpinBox::sectionConfigurations,
523 * they will be adapted before being applied.
524 *
525 * @post @ref m_sectionValues gets updated. The signal
526 * @ref MultiSpinBox::sectionValuesChanged() gets emitted if the
527 * new values are actually different from the old ones. */
528void MultiSpinBoxPrivate::setSectionValuesWithoutFurtherUpdating(const QList<double> &newSectionValues)
529{
530 if (newSectionValues.count() < 1) {
531 return;
532 }
533
534 const QListSizeType sectionCount = m_sectionConfigurations.count();
535
536 QList<double> fixedNewSectionValues = newSectionValues;
537
538 // Adapt the count of values:
539 while (fixedNewSectionValues.count() < sectionCount) {
540 // Add elements if there are not enough:
541 fixedNewSectionValues.append(MultiSpinBoxPrivate::defaultSectionValue);
542 }
543 while (fixedNewSectionValues.count() > sectionCount) {
544 // Remove elements if there are too many:
545 fixedNewSectionValues.removeLast();
546 }
547
548 // Make sure the new section values are
549 // valid (minimum <= value <= maximum):
550 MultiSpinBoxSection myConfig;
551 double rangeWidth;
552 double temp;
553 for (int i = 0; i < sectionCount; ++i) {
554 myConfig = m_sectionConfigurations.at(i);
555 fixedNewSectionValues[i] =
556 // Round value _before_ applying boundaries/wrapping.
557 roundToDigits(fixedNewSectionValues.at(i), myConfig.decimals());
558 if (myConfig.isWrapping()) {
559 rangeWidth = myConfig.maximum() - myConfig.minimum();
560 if (rangeWidth <= 0) {
561 // This is a special case.
562 // This happens when minimum == maximum (or
563 // if minimum > maximum, which is invalid).
564 fixedNewSectionValues[i] = myConfig.minimum();
565 } else {
566 // floating-point modulo (fmod) operation
567 temp = fmod(
568 // Dividend:
569 fixedNewSectionValues.at(i) - myConfig.minimum(),
570 // Divisor:
571 rangeWidth);
572 if (temp < 0) {
573 // Negative results shall be converted
574 // in positive results:
575 temp += rangeWidth;
576 }
577 temp += myConfig.minimum();
578 fixedNewSectionValues[i] = temp;
579 }
580 } else {
581 fixedNewSectionValues[i] = qBound(
582 // If there is no wrapping, simply bound:
583 myConfig.minimum(),
584 fixedNewSectionValues.at(i),
585 myConfig.maximum());
586 }
587 }
588
589 if (m_sectionValues != fixedNewSectionValues) {
590 m_sectionValues = fixedNewSectionValues;
591 Q_EMIT q_pointer->sectionValuesChanged(fixedNewSectionValues);
592 }
593}
594
595/** @brief Setter for @ref sectionValues property.
596 *
597 * @param newSectionValues The new section values. This list must have
598 * exactly as many items as @ref sectionConfigurations.
599 *
600 * The values will be bound between
601 * @ref MultiSpinBoxSection::minimum and
602 * @ref MultiSpinBoxSection::maximum. Their precision will be
603 * reduced to as many decimal places as given by
604 * @ref MultiSpinBoxSection::decimals. */
606{
607 d_pointer->setSectionValuesWithoutFurtherUpdating(newSectionValues);
608
609 // Update some internals…
610 d_pointer->updatePrefixValueSuffixText();
611
612 // Update the QLineEdit
613 { // Limit scope of QSignalBlocker
614 const QSignalBlocker blocker(lineEdit());
615 lineEdit()->setText(d_pointer->m_textBeforeCurrentValue //
616 + d_pointer->m_textOfCurrentValue //
617 + d_pointer->m_textAfterCurrentValue); //
618 // setCurrentIndexAndUpdateTextAndSelectValue(m_currentIndex);
619 }
620
621 // Make sure that the buttons for step-up and step-down are updated.
622 update();
623}
624
625/** @brief Focus handling for <em>Tab</em> respectively <em>Shift+Tab</em>.
626 *
627 * Reimplemented from base class.
628 *
629 * @note If it’s about moving the focus <em>within</em> this widget, the focus
630 * move is actually done. If it’s about moving the focus to <em>another</em>
631 * widget, the focus move is <em>not</em> actually done.
632 * The documentation of the base class is not very detailed. This
633 * reimplementation does not exactly behave as the documentation of the
634 * base class suggests. Especially, it handles directly the focus move
635 * <em>within</em> the widget itself. This was, however, the only working
636 * solution we found.
637 *
638 * @param next <tt>true</tt> stands for focus handling for <em>Tab</em>.
639 * <tt>false</tt> stands for focus handling for <em>Shift+Tab</em>.
640 *
641 * @returns <tt>true</tt> if the focus has actually been moved within
642 * this widget or if a move to another widget is possible. <tt>false</tt>
643 * otherwise. */
645{
646 if (next == true) { // Move focus forward (Tab)
647 if (d_pointer->m_currentIndex < (d_pointer->m_sectionConfigurations.count() - 1)) {
648 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_currentIndex + 1);
649 // Make sure that the buttons for step up and step down
650 // are updated.
651 update();
652 return true;
653 }
654 } else { // Move focus backward (Shift+Tab)
655 if (d_pointer->m_currentIndex > 0) {
656 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_currentIndex - 1);
657 // Make sure that the buttons for step up and step down
658 // are updated.
659 update();
660 return true;
661 }
662 }
663
664 // Make sure that the buttons for step up and step down are updated.
665 update();
666
667 // Return
668 return QWidget::focusNextPrevChild(next);
669}
670
671/** @brief Handles a <tt>QEvent::FocusOut</tt>.
672 *
673 * Reimplemented from base class.
674 *
675 * Updates the widget (except for windows that do not
676 * specify a <tt>focusPolicy()</tt>).
677 *
678 * @param event the <tt>QEvent::FocusOut</tt> to be handled. */
680{
682 switch (event->reason()) {
687 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0);
688 // Make sure that the buttons for step up and step down
689 // are updated.
690 update();
691 return;
697 default:
698 update();
699 return;
700 }
701}
702
703/** @brief Handles a <tt>QEvent::FocusIn</tt>.
704 *
705 * Reimplemented from base class.
706 *
707 * Updates the widget (except for windows that do not
708 * specify a <tt>focusPolicy()</tt>).
709 *
710 * @param event the <tt>QEvent::FocusIn</tt> to be handled. */
712{
714 switch (event->reason()) {
717 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0);
718 // Make sure that the buttons for step up and step down
719 // are updated.
720 update();
721 return;
723 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(d_pointer->m_sectionConfigurations.count() - 1);
724 // Make sure that the buttons for step up and step down
725 // are updated.
726 update();
727 return;
734 default:
735 update();
736 return;
737 }
738}
739
740/** @brief Increase or decrease the current section’s value.
741 *
742 * Reimplemented from base class.
743 *
744 * As of the base class’s documentation:
745 *
746 * > Virtual function that is called whenever the user triggers a step.
747 * > For example, pressing <tt>Qt::Key_Down</tt> will trigger a call
748 * > to <tt>stepBy(-1)</tt>, whereas pressing <tt>Qt::Key_PageUp</tt> will
749 * > trigger a call to <tt>stepBy(10)</tt>.
750 *
751 * @param steps Number of steps to be taken. The step size is
752 * the @ref MultiSpinBoxSection::singleStep of the current section. */
753void MultiSpinBox::stepBy(int steps)
754{
755 const QListSizeType currentIndex = d_pointer->m_currentIndex;
756 QList<double> myValues = sectionValues();
757 myValues[currentIndex] += steps * d_pointer->m_sectionConfigurations.at(currentIndex).singleStep();
758 // As explained in QAbstractSpinBox documentation:
759 // “Note that this function is called even if the resulting value will
760 // be outside the bounds of minimum and maximum. It’s this function’s
761 // job to handle these situations.”
762 // Therefore, the result has to be bound to the actual minimum and maximum
763 // values.
764 setSectionValues(myValues);
765 // Update the content of the QLineEdit and select the current
766 // value (as cursor text selection):
767 d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(currentIndex);
768 update(); // Make sure the buttons for step-up and step-down are updated.
769}
770
771/** @brief Updates the value of the current section.
772 *
773 * This slot is meant to be connected to the
774 * <tt>&QLineEdit::textChanged()</tt> signal of
775 * the <tt>MultiSpinBox::lineEdit()</tt> child widget.
776 * ,
777 * @param lineEditText The text of the <tt>lineEdit()</tt>. The value
778 * will be updated according to this parameter. Only changes in
779 * the <em>current</em> section’s value are expected, no changes in
780 * other sections. (If this parameter has an invalid value, a warning will
781 * be printed to stderr and the function returns without further action.) */
782void MultiSpinBoxPrivate::updateCurrentValueFromText(const QString &lineEditText)
783{
784 // Get the clean test. That means, we start with “text”, but
785 // we remove the m_currentSectionTextBeforeValue and the
786 // m_currentSectionTextAfterValue, so that only the text of
787 // the value itself remains.
788 QString cleanText = lineEditText;
789 if (cleanText.startsWith(m_textBeforeCurrentValue)) {
790 cleanText.remove(0, m_textBeforeCurrentValue.count());
791 } else {
792 // The text does not start with the correct characters.
793 // This is an error.
794 qWarning() << "The function" << __func__ //
795 << "in file" << __FILE__ //
796 << "near to line" << __LINE__ //
797 << "was called with the invalid “lineEditText“ argument “" << lineEditText //
798 << "” that does not start with the expected character sequence “" << m_textBeforeCurrentValue << ". " //
799 << "The call is ignored. This is a bug.";
800 return;
801 }
802 if (cleanText.endsWith(m_textAfterCurrentValue)) {
803 cleanText.chop(m_textAfterCurrentValue.count());
804 } else {
805 // The text does not start with the correct characters.
806 // This is an error.
807 qWarning() << "The function" << __func__ //
808 << "in file" << __FILE__ //
809 << "near to line" << __LINE__ //
810 << "was called with the invalid “lineEditText“ argument “" << lineEditText //
811 << "” that does not end with the expected character sequence “" << m_textAfterCurrentValue << ". " //
812 << "The call is ignored. This is a bug.";
813 return;
814 }
815
816 // Update…
817 bool ok;
818 QList<double> myValues = q_pointer->sectionValues();
819 myValues[m_currentIndex] = q_pointer->locale().toDouble(cleanText, &ok);
820 setSectionValuesWithoutFurtherUpdating(myValues);
821 // Make sure that the buttons for step up and step down are updated.
822 q_pointer->update();
823 // The lineEdit()->text() property is intentionally not updated because
824 // this function is meant to receive signals of the very same lineEdit().
825}
826
827/** @brief The main event handler.
828 *
829 * Reimplemented from base class.
830 *
831 * On <tt>QEvent::Type::LocaleChange</tt> it updates the spinbox content
832 * accordingly. Apart from that, it calls the implementation in the parent
833 * class.
834 *
835 * @returns The base class’s return value.
836 *
837 * @param event the event to be handled. */
839{
840 if (event->type() == QEvent::Type::LocaleChange) {
841 d_pointer->updatePrefixValueSuffixText();
842 d_pointer->m_validator->setPrefix(d_pointer->m_textBeforeCurrentValue);
843 d_pointer->m_validator->setSuffix(d_pointer->m_textAfterCurrentValue);
844 d_pointer->m_validator->setRange(
845 // Minimum
846 d_pointer->m_sectionConfigurations.at(d_pointer->m_currentIndex).minimum(),
847 // Maximum
848 d_pointer->m_sectionConfigurations.at(d_pointer->m_currentIndex).maximum());
849 lineEdit()->setText(d_pointer->m_textBeforeCurrentValue + d_pointer->m_textOfCurrentValue + d_pointer->m_textAfterCurrentValue);
850 }
852}
853
854/** @brief Updates the widget according to the new cursor position.
855 *
856 * This slot is meant to be connected to the
857 * <tt>QLineEdit::cursorPositionChanged()</tt> signal of
858 * the <tt>MultiSpinBox::lineEdit()</tt> child widget.
859 *
860 * @param oldPos the old cursor position (previous position)
861 * @param newPos the new cursor position (current position) */
862void MultiSpinBoxPrivate::reactOnCursorPositionChange(const int oldPos, const int newPos)
863{
864 Q_UNUSED(oldPos)
865
866 // We are working here with QString::length() and
867 // QLineEdit::cursorPosition(). Both are of type “int”, and both are
868 // measured in UTF-16 code units. While it feels quite uncomfortable
869 // to measure cursor positions in code _units_ and not at least in
870 // in code _points_, it does not matter for this code, as the behaviour
871 // is consistent between both usages.
872
873 if (isCursorTouchingCurrentSectionValue()) {
874 // We are within the value text of our current section value.
875 // There is nothing to do here.
876 return;
877 }
878
879 QSignalBlocker myBlocker(q_pointer->lineEdit());
880
881 // The new position is not at the current value, but the old one might
882 // have been. So maybe we have to correct the value, which might change
883 // its length. If the new cursor position is after this value, it will
884 // have to be adapted (if the value had been changed or alternated).
885 const QListSizeType oldTextLength = q_pointer->lineEdit()->text().length();
886 const bool mustAdjustCursorPosition = //
887 (newPos > (oldTextLength - m_textAfterCurrentValue.length()));
888
889 // Calculate in which section the cursor is
890 int sectionOfTheNewCursorPosition;
891 QStringLength reference = 0;
892 for (sectionOfTheNewCursorPosition = 0; //
893 sectionOfTheNewCursorPosition < m_sectionConfigurations.count() - 1; //
894 ++sectionOfTheNewCursorPosition //
895 ) {
896 reference += m_sectionConfigurations //
897 .at(sectionOfTheNewCursorPosition) //
898 .prefix() //
899 .length();
900 reference += formattedValue(sectionOfTheNewCursorPosition).length();
901 reference += m_sectionConfigurations //
902 .at(sectionOfTheNewCursorPosition) //
903 .suffix() //
904 .length();
905 if (newPos <= reference) {
906 break;
907 }
908 }
909
910 updatePrefixValueSuffixText();
911 setCurrentIndexWithoutUpdatingText(sectionOfTheNewCursorPosition);
912 q_pointer->lineEdit()->setText(m_textBeforeCurrentValue //
913 + m_textOfCurrentValue //
914 + m_textAfterCurrentValue);
915 int correctedCursorPosition = newPos;
916 if (mustAdjustCursorPosition) {
917 correctedCursorPosition = //
918 static_cast<int>(newPos //
919 + q_pointer->lineEdit()->text().length() //
920 - oldTextLength);
921 }
922 q_pointer->lineEdit()->setCursorPosition(correctedCursorPosition);
923
924 // Make sure that the buttons for step up and step down are updated.
925 q_pointer->update();
926}
927
928/** @brief Constructor
929 *
930 * @param w The widget to which the newly created object will correspond. */
931AccessibleMultiSpinBox::AccessibleMultiSpinBox(MultiSpinBox *w)
932 : QAccessibleWidget(w, QAccessible::Role::SpinBox)
933{
934}
935
936/** @brief Destructor */
937AccessibleMultiSpinBox::~AccessibleMultiSpinBox()
938{
939}
940
941/** @brief Factory function.
942 *
943 * This signature of this function is exactly as defined by
944 * <tt>QAccessible::InterfaceFactory</tt>. A pointer to this function
945 * can therefore be passed to <tt>QAccessible::installFactory()</tt>.
946 *
947 * @param classname The class name for which an interface is requested
948 * @param object The object for which an interface is requested
949 *
950 * @returns If this class corresponds to the request, it returns an object
951 * of this class. Otherwise, a null-pointer will be returned. */
952QAccessibleInterface *AccessibleMultiSpinBox::factory(const QString &classname, QObject *object)
953{
954 QAccessibleInterface *interface = nullptr;
955 const QString multiSpinBoxClassName = QString::fromUtf8(
956 // className() returns const char *. Its encoding is not documented.
957 // Hopefully, as we use UTF8 in this library as “input character set”
958 // and also as “Narrow execution character set”, the encoding
959 // might be also UTF8…
960 MultiSpinBox::staticMetaObject.className());
961 MultiSpinBox *myMultiSpinBox = qobject_cast<MultiSpinBox *>(object);
962 if ((classname == multiSpinBoxClassName) && myMultiSpinBox) {
963 interface = new AccessibleMultiSpinBox(myMultiSpinBox);
964 }
965 return interface;
966}
967
968/** @brief Current implementation does nothing.
969 *
970 * Reimplemented from base class.
971 *
972 * @internal
973 *
974 * This class has to be necessarily reimplemented because the base
975 * class’s implementation is incompatible with <em>this</em> class
976 * and could produce undefined behaviour.
977 *
978 * If this function would be reimplemented in the future, here
979 * is the specification:
980 *
981 * @note Qt’s own child classes use this function to implement <tt>Ctrl-U</tt>.
982 * But this not relevant here, because this class has its own implementation
983 * for keyboard event handling (and currently does not even handle
984 * <tt>Ctrl-U</tt> at all).
985 *
986 * <tt>brief</tt> Clears the value of the current section.
987 *
988 * The other sections and also the prefix and suffix of the current
989 * section stay visible.
990 *
991 * The base class is documented as:
992 * <em>Clears the lineedit of all text but prefix and suffix.</em> The
993 * semantic of this reimplementation is slightly different; it is however
994 * the same semantic that also QDateTimeEdit, another child class
995 * of <tt>QAbstractSpinBox</tt>, applies. */
997{
998}
999
1000} // namespace PerceptualColor
The configuration of a single section within a MultiSpinBox.
double maximum() const
The maximum possible value of the section.
double minimum() const
The minimum possible value of the section.
bool isWrapping() const
Holds whether or not MultiSpinBox::sectionValues wrap around when they reaches minimum or maximum.
A spin box that can hold multiple sections (each with its own value) at the same time.
Q_INVOKABLE MultiSpinBox(QWidget *parent=nullptr)
Constructor.
virtual bool event(QEvent *event) override
The main event handler.
virtual QSize minimumSizeHint() const override
The recommended minimum size for the widget.
void addActionButton(QAction *action, QLineEdit::ActionPosition position)
Adds to the widget a button associated with the given action.
virtual bool focusNextPrevChild(bool next) override
Focus handling for Tab respectively Shift+Tab.
virtual void focusInEvent(QFocusEvent *event) override
Handles a QEvent::FocusIn.
virtual QAbstractSpinBox::StepEnabled stepEnabled() const override
Virtual function that determines whether stepping up and down is legal at any given time.
void setSectionValues(const QList< double > &newSectionValues)
Setter for sectionValues property.
Q_INVOKABLE QList< PerceptualColor::MultiSpinBoxSection > sectionConfigurations() const
Returns the configuration of all sections.
virtual QSize sizeHint() const override
The recommended size for the widget.
virtual void clear() override
Current implementation does nothing.
virtual ~MultiSpinBox() noexcept override
Default destructor.
virtual void focusOutEvent(QFocusEvent *event) override
Handles a QEvent::FocusOut.
Q_INVOKABLE void setSectionConfigurations(const QList< PerceptualColor::MultiSpinBoxSection > &newSectionConfigurations)
Sets the configuration for the sections.
virtual void stepBy(int steps) override
Increase or decrease the current section’s value.
virtual void changeEvent(QEvent *event) override
Handle state changes.
QList< double > sectionValues
A list containing the values of all sections.
The namespace of this library.
virtual void changeEvent(QEvent *event) override
void editingFinished()
virtual bool event(QEvent *event) override
virtual void focusInEvent(QFocusEvent *event) override
virtual void focusOutEvent(QFocusEvent *event) override
virtual void initStyleOption(QStyleOptionSpinBox *option) const const
QLineEdit * lineEdit() const const
void installFactory(InterfaceFactory factory)
int horizontalAdvance(QChar ch) const const
QAction * addAction(const QIcon &icon, ActionPosition position)
void cursorPositionChanged(int oldPos, int newPos)
void setValidator(const QValidator *v)
virtual QSize sizeHint() const const override
void setText(const QString &)
void textChanged(const QString &text)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
qsizetype count() const const
void removeLast()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
int height() const const
void setWidth(int width)
int width() const const
qsizetype count() const const
QString & append(QChar ch)
void chop(qsizetype n)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
qsizetype length() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
PM_SmallIconSize
virtual int pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const const=0
virtual QSize sizeFromContents(ContentsType type, const QStyleOption *option, const QSize &contentsSize, const QWidget *widget) const const=0
ShortcutFocusReason
void ensurePolished() const const
virtual bool focusNextPrevChild(bool next)
QFontMetrics fontMetrics() const const
QStyle * style() const const
void update()
void updateGeometry()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:36 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.