Allow adaptive decimal stepping for QSpinBox and QDoubleSpinBox

Adds the feature of adaptive decimal step sizes for the QSpinBox and
QDoubleSpinBox. By performing a calculation in
QAbstractSpinBox::stepBy() we continuously set the step size one power
of ten below the current value.

So when the value is 1100, the step is set to 100, so stepping up once
increases it to 1200. For 1200 stepping up takes it to 1300. For
negative values stepping down from -1100 goes to -1200.

It also works for all decimal values. 0.041 is increased to 0.042 by
stepping once, and so on.

The step direction is taken into account to handle edges cases, so that
stepping down from 100 takes the value to 99 instead of 90. Thus, a
step up followed by a step down -- or vice versa -- lands on the
starting value; 99 -> 100 -> 99.

Setting this property effectively disregards singleStep, but preserves
its value so that it takes effect again when adaptive decimal step is
disabled.

Adaptive decimal step allows values to be easily set to reasonable
levels. If the spin box value is 12000, changing to 13000 often makes
more sense than to 12001. The feature is turned off be default, when
single stepping is desired.

The accelerated property allows values to be changed quickly, as well,
but it is imprecise. Holding down the button makes it hard to land on
an even thousand, like 12000 or 13000. Often you end up somewhere
nearby and would need a second adjustment to get to an even hundred or
thousand.

[ChangeLog][QtWidgets] Add option of adaptive decimal step size for
QSpinBox and QDoubleSpinBox.

Change-Id: I9f286479b821e240c8ea05c238932fc128c582bb
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
bb10
Harald Nordgren 2016-03-28 23:07:46 +02:00 committed by Shawn Rutledge
parent a692d7cd28
commit 97e40a5409
7 changed files with 358 additions and 1 deletions

View File

@ -639,7 +639,15 @@ void QAbstractSpinBox::stepBy(int steps)
e = AlwaysEmit;
}
if (!dontstep) {
d->setValue(d->bound(d->value + (d->singleStep * steps), old, steps), e);
QVariant singleStep;
switch (d->stepType) {
case QAbstractSpinBox::StepType::AdaptiveDecimalStepType:
singleStep = d->calculateAdaptiveDecimalStep(steps);
break;
default:
singleStep = d->singleStep;
}
d->setValue(d->bound(d->value + (singleStep * steps), old, steps), e);
} else if (e == AlwaysEmit) {
d->emitSignals(e, old);
}
@ -1898,6 +1906,11 @@ void QAbstractSpinBoxPrivate::clearCache() const
cachedState = QValidator::Acceptable;
}
QVariant QAbstractSpinBoxPrivate::calculateAdaptiveDecimalStep(int steps) const
{
Q_UNUSED(steps)
return singleStep;
}
// --- QSpinBoxValidator ---

View File

@ -127,6 +127,11 @@ public:
virtual void fixup(QString &input) const;
virtual void stepBy(int steps);
enum StepType {
DefaultStepType,
AdaptiveDecimalStepType
};
public Q_SLOTS:
void stepUp();
void stepDown();

View File

@ -122,6 +122,8 @@ public:
static int variantCompare(const QVariant &arg1, const QVariant &arg2);
static QVariant variantBound(const QVariant &min, const QVariant &value, const QVariant &max);
virtual QVariant calculateAdaptiveDecimalStep(int steps) const;
QLineEdit *edit;
QString prefix, suffix, specialValueText;
QVariant value, minimum, maximum, singleStep;
@ -143,6 +145,7 @@ public:
uint cleared : 1;
uint ignoreUpdateEdit : 1;
QAbstractSpinBox::CorrectionMode correctionMode;
QAbstractSpinBox::StepType stepType = QAbstractSpinBox::StepType::DefaultStepType;
int acceleration;
QStyle::SubControl hoverControl;
QRect hoverRect;

View File

@ -45,6 +45,8 @@
#include <qvalidator.h>
#include <qdebug.h>
#include <algorithm>
#include <cmath>
#include <float.h>
QT_BEGIN_NAMESPACE
@ -75,6 +77,8 @@ public:
}
int displayIntegerBase;
QVariant calculateAdaptiveDecimalStep(int steps) const override;
};
class QDoubleSpinBoxPrivate : public QAbstractSpinBoxPrivate
@ -100,6 +104,8 @@ public:
// When fiddling with the decimals property, we may lose precision in these properties.
double actualMin;
double actualMax;
QVariant calculateAdaptiveDecimalStep(int steps) const override;
};
@ -414,6 +420,41 @@ void QSpinBox::setRange(int minimum, int maximum)
d->setRange(QVariant(minimum), QVariant(maximum));
}
/*!
Sets the step type for the spin box: single step or adaptive
decimal step.
Adaptive decimal step means that the step size will continuously be
adjusted to one power of ten below the current \l value. So when
the value is 1100, the step is set to 100, so stepping up once
increases it to 1200. For 1200 stepping up takes it to 1300. For
negative values, stepping down from -1100 goes to -1200.
Step direction is taken into account to handle edges cases, so
that stepping down from 100 takes the value to 99 instead of 90.
Thus a step up followed by a step down -- or vice versa -- always
lands on the starting value; 99 -> 100 -> 99.
Setting this will cause the spin box to disregard the value of
\l singleStep, although it is preserved so that \l singleStep
comes into effect if adaptive decimal step is later turned off.
\sa QAbstractSpinBox::groupSeparator()
\since 5.12
*/
void QSpinBox::setStepType(QAbstractSpinBox::StepType stepType)
{
Q_D(QSpinBox);
d->stepType = stepType;
}
QAbstractSpinBox::StepType QSpinBox::stepType() const
{
Q_D(const QSpinBox);
return d->stepType;
}
/*!
\property QSpinBox::displayIntegerBase
@ -846,6 +887,44 @@ void QDoubleSpinBox::setRange(double minimum, double maximum)
d->setRange(QVariant(d->round(minimum)), QVariant(d->round(maximum)));
}
/*!
Sets the step type for the spin box: single step or adaptive
decimal step.
Adaptive decimal step means that the step size will continuously be
adjusted to one power of ten below the current \l value. So when
the value is 1100, the step is set to 100, so stepping up once
increases it to 1200. For 1200 stepping up takes it to 1300. For
negative values, stepping down from -1100 goes to -1200.
It also works for any decimal values, 0.041 is increased to 0.042
by stepping once.
Step direction is taken into account to handle edges cases, so
that stepping down from 100 takes the value to 99 instead of 90.
Thus a step up followed by a step down -- or vice versa -- always
lands on the starting value; 99 -> 100 -> 99.
Setting this will cause the spin box to disregard the value of
\l singleStep, although it is preserved so that \l singleStep
comes into effect if adaptive decimal step is later turned off.
\sa QAbstractSpinBox::groupSeparator()
\since 5.12
*/
void QDoubleSpinBox::setStepType(StepType stepType)
{
Q_D(QDoubleSpinBox);
d->stepType = stepType;
}
QAbstractSpinBox::StepType QDoubleSpinBox::stepType() const
{
Q_D(const QDoubleSpinBox);
return d->stepType;
}
/*!
\property QDoubleSpinBox::decimals
@ -1078,6 +1157,22 @@ QVariant QSpinBoxPrivate::validateAndInterpret(QString &input, int &pos,
return cachedValue;
}
QVariant QSpinBoxPrivate::calculateAdaptiveDecimalStep(int steps) const
{
const int intValue = value.toInt();
const int absValue = qAbs(intValue);
if (absValue < 100)
return 1;
const bool valueNegative = intValue < 0;
const bool stepsNegative = steps < 0;
const int signCompensation = (valueNegative == stepsNegative) ? 0 : 1;
const int log = static_cast<int>(std::log10(absValue - signCompensation)) - 1;
return static_cast<int>(std::pow(10, log));
}
// --- QDoubleSpinBoxPrivate ---
/*!
@ -1303,6 +1398,27 @@ QString QDoubleSpinBoxPrivate::textFromValue(const QVariant &f) const
return q->textFromValue(f.toDouble());
}
QVariant QDoubleSpinBoxPrivate::calculateAdaptiveDecimalStep(int steps) const
{
const double doubleValue = value.toDouble();
const double minStep = std::pow(10, -decimals);
double absValue = qAbs(doubleValue);
if (absValue < minStep)
return minStep;
const bool valueNegative = doubleValue < 0;
const bool stepsNegative = steps < 0;
if (valueNegative != stepsNegative)
absValue /= 1.01;
const double shift = std::pow(10, 1 - std::floor(std::log10(absValue)));
const double absRounded = round(absValue * shift) / shift;
const double log = floorf(std::log10(absRounded)) - 1;
return std::max(minStep, std::pow(10, log));
}
/*! \reimp */
bool QSpinBox::event(QEvent *event)
{

View File

@ -58,6 +58,7 @@ class Q_WIDGETS_EXPORT QSpinBox : public QAbstractSpinBox
Q_PROPERTY(int minimum READ minimum WRITE setMinimum)
Q_PROPERTY(int maximum READ maximum WRITE setMaximum)
Q_PROPERTY(int singleStep READ singleStep WRITE setSingleStep)
Q_PROPERTY(StepType stepType READ stepType WRITE setStepType)
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged USER true)
Q_PROPERTY(int displayIntegerBase READ displayIntegerBase WRITE setDisplayIntegerBase)
@ -86,6 +87,9 @@ public:
void setRange(int min, int max);
StepType stepType() const;
void setStepType(StepType stepType);
int displayIntegerBase() const;
void setDisplayIntegerBase(int base);
@ -121,6 +125,7 @@ class Q_WIDGETS_EXPORT QDoubleSpinBox : public QAbstractSpinBox
Q_PROPERTY(double minimum READ minimum WRITE setMinimum)
Q_PROPERTY(double maximum READ maximum WRITE setMaximum)
Q_PROPERTY(double singleStep READ singleStep WRITE setSingleStep)
Q_PROPERTY(StepType stepType READ stepType WRITE setStepType)
Q_PROPERTY(double value READ value WRITE setValue NOTIFY valueChanged USER true)
public:
explicit QDoubleSpinBox(QWidget *parent = nullptr);
@ -147,6 +152,9 @@ public:
void setRange(double min, double max);
StepType stepType() const;
void setStepType(StepType stepType);
int decimals() const;
void setDecimals(int prec);

View File

@ -31,6 +31,7 @@
#include <qapplication.h>
#include <limits.h>
#include <cmath>
#include <float.h>
#include <qspinbox.h>
@ -135,6 +136,8 @@ private slots:
void setGroupSeparatorShown_data();
void setGroupSeparatorShown();
void adaptiveDecimalStep();
public slots:
void valueChangedHelper(const QString &);
void valueChangedHelper(double);
@ -1139,5 +1142,133 @@ void tst_QDoubleSpinBox::setGroupSeparatorShown()
QCOMPARE(spinBox.value()+1000, 33000.44);
}
void tst_QDoubleSpinBox::adaptiveDecimalStep()
{
DoubleSpinBox spinBox;
spinBox.setRange(-100000, 100000);
spinBox.setDecimals(4);
spinBox.setStepType(DoubleSpinBox::StepType::AdaptiveDecimalStepType);
// Positive values
spinBox.setValue(0);
// Go from 0 to 0.01
for (double i = 0; i < 0.00999; i += 0.0001) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Go from 0.01 to 0.1
for (double i = 0.01; i < 0.0999; i += 0.001) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Go from 0.1 to 1
for (double i = 0.1; i < 0.999; i += 0.01) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Go from 1 to 10
for (double i = 1; i < 9.99; i += 0.1) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Go from 10 to 100
for (int i = 10; i < 100; i++) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Go from 100 to 1000
for (int i = 100; i < 1000; i += 10) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Go from 1000 to 10000
for (int i = 1000; i < 10000; i += 100) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Test decreasing the values now
// Go from 10000 down to 1000
for (int i = 10000; i > 1000; i -= 100) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Go from 1000 down to 100
for (int i = 1000; i > 100; i -= 10) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Negative values
spinBox.setValue(0);
// Go from 0 to -0.01
for (double i = 0; i > -0.00999; i -= 0.0001) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Go from -0.01 to -0.1
for (double i = -0.01; i > -0.0999; i -= 0.001) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Go from -0.1 to -1
for (double i = -0.1; i > -0.999; i -= 0.01) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Go from -1 to -10
for (double i = -1; i > -9.99; i -= 0.1) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Go from -10 to -100
for (int i = -10; i > -100; i--) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Go from -100 to -1000
for (int i = -100; i > -1000; i -= 10) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Go from 1000 to 10000
for (int i = -1000; i > -10000; i -= 100) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(-1);
}
// Test increasing the values now
// Go from -10000 up to -1000
for (int i = -10000; i < -1000; i += 100) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
// Go from -1000 up to -100
for (int i = -1000; i < -100; i += 10) {
QVERIFY(qFuzzyCompare(spinBox.value(), i));
spinBox.stepBy(1);
}
}
QTEST_MAIN(tst_QDoubleSpinBox)
#include "tst_qdoublespinbox.moc"

View File

@ -145,6 +145,8 @@ private slots:
void wheelEvents();
void adaptiveDecimalStep();
public slots:
void valueChangedHelper(const QString &);
void valueChangedHelper(int);
@ -1240,5 +1242,84 @@ void tst_QSpinBox::wheelEvents()
#endif
}
void tst_QSpinBox::adaptiveDecimalStep()
{
SpinBox spinBox;
spinBox.setRange(-100000, 100000);
spinBox.setStepType(SpinBox::StepType::AdaptiveDecimalStepType);
// Positive values
spinBox.setValue(0);
// Go from 0 to 100
for (int i = 0; i < 100; i++) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(1);
}
// Go from 100 to 1000
for (int i = 100; i < 1000; i += 10) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(1);
}
// Go from 1000 to 10000
for (int i = 1000; i < 10000; i += 100) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(1);
}
// Test decreasing the values now
// Go from 10000 down to 1000
for (int i = 10000; i > 1000; i -= 100) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(-1);
}
// Go from 1000 down to 100
for (int i = 1000; i > 100; i -= 10) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(-1);
}
// Negative values
spinBox.setValue(0);
// Go from 0 to -100
for (int i = 0; i > -100; i--) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(-1);
}
// Go from -100 to -1000
for (int i = -100; i > -1000; i -= 10) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(-1);
}
// Go from 1000 to 10000
for (int i = -1000; i > -10000; i -= 100) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(-1);
}
// Test increasing the values now
// Go from -10000 up to -1000
for (int i = -10000; i < -1000; i += 100) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(1);
}
// Go from -1000 up to -100
for (int i = -1000; i < -100; i += 10) {
QCOMPARE(spinBox.value(), i);
spinBox.stepBy(1);
}
}
QTEST_MAIN(tst_QSpinBox)
#include "tst_qspinbox.moc"