Apply Qt::ControlModifier to all spin box interactions

A step modifier already existed when scrolling with the
Qt::ControlModifier held. This patch applies this functionality to
other methods of stepping a spin box.

Holding the modifier increases the step rate when:
- scrolling;
- pressing the up/down keys;
- pressing the spin box up/down buttons.

[ChangeLog][QtWidgets][QAbstractSpinBox] The Qt::ControlModifier
increases the number of steps a QAbstractSpinBox takes for the
following interactions: scrolling, up/down keyboard keys and the spin
box buttons. Previously, Qt::ControlModifier only affected scrolling.

Task-number: QTBUG-67380
Change-Id: Icc8754d5c007da0771bfaef113603a2f334dd494
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
This commit is contained in:
Nathan Collins 2018-05-14 19:22:22 +01:00
parent cb3e85fc9a
commit 2d1e1f569b
5 changed files with 1279 additions and 33 deletions

View File

@ -100,6 +100,12 @@ QT_BEGIN_NAMESPACE
integer value to signify how many steps were taken. E.g. Pressing
Qt::Key_Down will trigger a call to stepBy(-1).
When the user triggers a step whilst holding the Qt::ControlModifier,
QAbstractSpinBox steps by 10 instead of making a single step. This
step modifier affects wheel events, key events and interaction with
the spinbox buttons. Note that on macOS, Control corresponds to the
Command key.
QAbstractSpinBox also provide a virtual function stepEnabled() to
determine whether stepping up/down is allowed at any point. This
function returns a bitset of StepEnabled.
@ -1015,6 +1021,8 @@ void QAbstractSpinBox::keyPressEvent(QKeyEvent *event)
const bool up = (event->key() == Qt::Key_PageUp || event->key() == Qt::Key_Up);
if (!(stepEnabled() & (up ? StepUpEnabled : StepDownEnabled)))
return;
if (!isPgUpOrDown && (event->modifiers() & d->stepModifier))
steps *= 10;
if (!up)
steps *= -1;
if (style()->styleHint(QStyle::SH_SpinBox_AnimateButton, 0, this)) {
@ -1141,11 +1149,24 @@ void QAbstractSpinBox::keyReleaseEvent(QKeyEvent *event)
void QAbstractSpinBox::wheelEvent(QWheelEvent *event)
{
Q_D(QAbstractSpinBox);
#ifdef Q_OS_MACOS
// If the event comes from a real mouse wheel, rather than a track pad
// (Qt::MouseEventSynthesizedBySystem), the shift modifier changes the
// scroll orientation to horizontal.
// Convert horizontal events back to vertical whilst shift is held.
if ((event->modifiers() & Qt::ShiftModifier)
&& event->source() == Qt::MouseEventNotSynthesized) {
d->wheelDeltaRemainder += event->angleDelta().x();
} else {
d->wheelDeltaRemainder += event->angleDelta().y();
}
#else
d->wheelDeltaRemainder += event->angleDelta().y();
#endif
const int steps = d->wheelDeltaRemainder / 120;
d->wheelDeltaRemainder -= steps * 120;
if (stepEnabled() & (steps > 0 ? StepUpEnabled : StepDownEnabled))
stepBy(event->modifiers() & Qt::ControlModifier ? steps * 10 : steps);
stepBy(event->modifiers() & d->stepModifier ? steps * 10 : steps);
event->accept();
}
#endif
@ -1245,18 +1266,19 @@ void QAbstractSpinBox::timerEvent(QTimerEvent *event)
}
if (doStep) {
const bool increaseStepRate = QGuiApplication::keyboardModifiers() & d->stepModifier;
const StepEnabled st = stepEnabled();
if (d->buttonState & Up) {
if (!(st & StepUpEnabled)) {
d->reset();
} else {
stepBy(1);
stepBy(increaseStepRate ? 10 : 1);
}
} else if (d->buttonState & Down) {
if (!(st & StepDownEnabled)) {
d->reset();
} else {
stepBy(-1);
stepBy(increaseStepRate ? -10 : -1);
}
}
return;
@ -1384,8 +1406,9 @@ QAbstractSpinBoxPrivate::QAbstractSpinBoxPrivate()
cachedState(QValidator::Invalid), pendingEmit(false), readOnly(false), wrapping(false),
ignoreCursorPositionChanged(false), frame(true), accelerate(false), keyboardTracking(true),
cleared(false), ignoreUpdateEdit(false), correctionMode(QAbstractSpinBox::CorrectToPreviousValue),
acceleration(0), hoverControl(QStyle::SC_None), buttonSymbols(QAbstractSpinBox::UpDownArrows), validator(0),
showGroupSeparator(0), wheelDeltaRemainder(0)
stepModifier(Qt::ControlModifier), acceleration(0), hoverControl(QStyle::SC_None),
buttonSymbols(QAbstractSpinBox::UpDownArrows), validator(0), showGroupSeparator(0),
wheelDeltaRemainder(0)
{
}
@ -1636,7 +1659,10 @@ void QAbstractSpinBoxPrivate::updateState(bool up, bool fromKeyboard /* = false
: QAbstractSpinBox::StepDownEnabled))) {
spinClickThresholdTimerId = q->startTimer(spinClickThresholdTimerInterval);
buttonState = (up ? Up : Down) | (fromKeyboard ? Keyboard : Mouse);
q->stepBy(up ? 1 : -1);
int steps = up ? 1 : -1;
if (QGuiApplication::keyboardModifiers() & stepModifier)
steps *= 10;
q->stepBy(steps);
#ifndef QT_NO_ACCESSIBILITY
QAccessibleValueChangeEvent event(q, value);
QAccessible::updateAccessibility(&event);

View File

@ -146,6 +146,7 @@ public:
uint ignoreUpdateEdit : 1;
QAbstractSpinBox::CorrectionMode correctionMode;
QAbstractSpinBox::StepType stepType = QAbstractSpinBox::StepType::DefaultStepType;
Qt::KeyboardModifier stepModifier = Qt::ControlModifier;
int acceleration;
QStyle::SubControl hoverControl;
QRect hoverRect;

View File

@ -70,6 +70,7 @@
#include <QSignalSpy>
#include <QTestEventList>
#include <QDateEdit>
#include <QProxyStyle>
#include <private/qdatetimeedit_p.h>
@ -93,6 +94,26 @@ public:
friend class tst_QDateTimeEdit;
};
class PressAndHoldStyle : public QProxyStyle
{
Q_OBJECT
public:
using QProxyStyle::QProxyStyle;
int styleHint(QStyle::StyleHint hint, const QStyleOption *option = nullptr,
const QWidget *widget = nullptr, QStyleHintReturn *returnData = nullptr) const override
{
switch (hint) {
case QStyle::SH_SpinBox_ClickAutoRepeatRate:
return 5;
case QStyle::SH_SpinBox_ClickAutoRepeatThreshold:
return 10;
default:
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
}
};
class tst_QDateTimeEdit : public QObject
{
Q_OBJECT
@ -205,9 +226,8 @@ private slots:
void reverseTest();
void ddMMMMyyyy();
#if QT_CONFIG(wheelevent)
void wheelEvent_data();
void wheelEvent();
#endif
void specialValueCornerCase();
void cursorPositionOnInit();
@ -245,14 +265,99 @@ private slots:
void dateEditCorrectSectionSize_data();
void dateEditCorrectSectionSize();
#endif
void stepModifierKeys_data();
void stepModifierKeys();
void stepModifierButtons_data();
void stepModifierButtons();
void stepModifierPressAndHold_data();
void stepModifierPressAndHold();
private:
EditorDateEdit* testWidget;
QWidget *testFocusWidget;
};
typedef QList<QDate> DateList;
typedef QList<QTime> TimeList;
typedef QList<Qt::Key> KeyList;
static QLatin1String modifierToName(Qt::KeyboardModifier modifier)
{
switch (modifier) {
case Qt::NoModifier:
return QLatin1Literal("No");
break;
case Qt::ControlModifier:
return QLatin1Literal("Ctrl");
break;
case Qt::ShiftModifier:
return QLatin1Literal("Shift");
break;
default:
qFatal("Unexpected keyboard modifier");
return QLatin1String();
}
}
static QLatin1String sectionToName(const QDateTimeEdit::Section section)
{
switch (section) {
case QDateTimeEdit::SecondSection:
return QLatin1Literal("Second");
case QDateTimeEdit::MinuteSection:
return QLatin1Literal("Minute");
case QDateTimeEdit::HourSection:
return QLatin1Literal("Hours");
case QDateTimeEdit::DaySection:
return QLatin1Literal("Day");
case QDateTimeEdit::MonthSection:
return QLatin1Literal("Month");
case QDateTimeEdit::YearSection:
return QLatin1Literal("Year");
default:
qFatal("Unexpected section");
return QLatin1String();
}
}
static QDate stepDate(const QDate& startDate, const QDateTimeEdit::Section section,
const int steps)
{
switch (section) {
case QDateTimeEdit::DaySection:
return startDate.addDays(steps);
case QDateTimeEdit::MonthSection:
return startDate.addMonths(steps);
case QDateTimeEdit::YearSection:
return startDate.addYears(steps);
default:
qFatal("Unexpected section");
return QDate();
}
}
static QTime stepTime(const QTime& startTime, const QDateTimeEdit::Section section,
const int steps)
{
switch (section) {
case QDateTimeEdit::SecondSection:
return startTime.addSecs(steps);
case QDateTimeEdit::MinuteSection:
return QTime(startTime.hour(),
startTime.minute() + steps,
startTime.second());
case QDateTimeEdit::HourSection:
return QTime(startTime.hour() + steps,
startTime.minute(),
startTime.second());
default:
qFatal("Unexpected section");
return QTime();
}
}
// Testing get/set functions
void tst_QDateTimeEdit::getSetCheck()
{
@ -3000,20 +3105,154 @@ void tst_QDateTimeEdit::ddMMMMyyyy()
QCOMPARE(testWidget->lineEdit()->text(), "01." + QDate::longMonthName(1) + ".200");
}
void tst_QDateTimeEdit::wheelEvent_data()
{
#if QT_CONFIG(wheelevent)
QTest::addColumn<QPoint>("angleDelta");
QTest::addColumn<int>("qt4Delta");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<Qt::MouseEventSource>("source");
QTest::addColumn<QDateTimeEdit::Section>("section");
QTest::addColumn<QDate>("startDate");
QTest::addColumn<DateList>("expectedDates");
const auto fractions = {false, true};
const auto directions = {true, false};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
const auto sources = {Qt::MouseEventNotSynthesized,
Qt::MouseEventSynthesizedBySystem,
Qt::MouseEventSynthesizedByQt,
Qt::MouseEventSynthesizedByApplication};
const auto sections = {QDateTimeEdit::DaySection,
QDateTimeEdit::MonthSection,
QDateTimeEdit::YearSection};
for (auto fraction : fractions) {
for (auto up : directions) {
const QDate startDate(2000, up ? 2 : 12, 17);
const int units = (fraction ? 60 : 120) * (up ? 1 : -1);
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
for (auto source : sources) {
#ifdef Q_OS_MACOS
QPoint angleDelta;
if ((modifier & Qt::ShiftModifier) &&
source == Qt::MouseEventNotSynthesized) {
// On macOS the Shift modifier converts vertical
// mouse wheel events to horizontal.
angleDelta = { units, 0 };
} else {
// However, this is not the case for trackpad scroll
// events.
angleDelta = { 0, units };
}
#else
const QPoint angleDelta(0, units);
#endif
QLatin1String sourceName;
switch (source) {
case Qt::MouseEventNotSynthesized:
sourceName = QLatin1Literal("NotSynthesized");
break;
case Qt::MouseEventSynthesizedBySystem:
sourceName = QLatin1Literal("SynthesizedBySystem");
break;
case Qt::MouseEventSynthesizedByQt:
sourceName = QLatin1Literal("SynthesizedByQt");
break;
case Qt::MouseEventSynthesizedByApplication:
sourceName = QLatin1Literal("SynthesizedByApplication");
break;
default:
qFatal("Unexpected wheel event source");
continue;
}
for (const auto section : sections) {
DateList expectedDates;
if (fraction)
expectedDates << startDate;
const auto expectedDate = stepDate(startDate, section, steps);
if (!expectedDate.isValid())
continue;
expectedDates << expectedDate;
const QLatin1String sectionName = sectionToName(section);
QTest::addRow("%s%s%sWith%sKeyboardModifier%s",
fraction ? "half" : "full",
up ? "Up" : "Down",
sectionName.latin1(),
modifierName.latin1(),
sourceName.latin1())
<< angleDelta
<< units
<< modifiers
<< source
<< section
<< startDate
<< expectedDates;
}
}
}
}
}
#else
QSKIP("Built with --no-feature-wheelevent");
#endif
}
void tst_QDateTimeEdit::wheelEvent()
{
testWidget->setDisplayFormat("dddd/MM");
testWidget->setDate(QDate(2000, 2, 21));
testWidget->setCurrentSection(QDateTimeEdit::DaySection);
QWheelEvent w(testWidget->lineEdit()->geometry().center(), 120, 0, 0);
qApp->sendEvent(testWidget, &w);
QCOMPARE(testWidget->date(), QDate(2000, 2, 22));
testWidget->setCurrentSection(QDateTimeEdit::MonthSection);
qApp->sendEvent(testWidget, &w);
QCOMPARE(testWidget->date(), QDate(2000, 3, 22));
}
#if QT_CONFIG(wheelevent)
QFETCH(QPoint, angleDelta);
QFETCH(int, qt4Delta);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(Qt::MouseEventSource, source);
QFETCH(QDateTimeEdit::Section, section);
QFETCH(QDate, startDate);
QFETCH(DateList, expectedDates);
EditorDateEdit edit(0);
edit.setDate(startDate);
edit.setCurrentSection(section);
QWheelEvent event(QPointF(), QPointF(), QPoint(), angleDelta, qt4Delta,
Qt::Vertical, Qt::NoButton, modifiers, Qt::NoScrollPhase,
source);
QCOMPARE(edit.date(), startDate);
for (QDate expected : expectedDates) {
qApp->sendEvent(&edit, &event);
QCOMPARE(edit.date(), expected);
}
#else
QSKIP("Built with --no-feature-wheelevent");
#endif // QT_CONFIG(wheelevent)
}
void tst_QDateTimeEdit::specialValueCornerCase()
{
@ -3735,5 +3974,238 @@ void tst_QDateTimeEdit::dateEditCorrectSectionSize()
}
#endif
void tst_QDateTimeEdit::stepModifierKeys_data()
{
QTest::addColumn<QDate>("startDate");
QTest::addColumn<QDateTimeEdit::Section>("section");
QTest::addColumn<QTestEventList>("keys");
QTest::addColumn<QDate>("expectedDate");
const auto keyList = {Qt::Key_Up, Qt::Key_Down};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
const auto sections = {QDateTimeEdit::DaySection,
QDateTimeEdit::MonthSection,
QDateTimeEdit::YearSection};
for (auto key : keyList) {
const bool up = key == Qt::Key_Up;
Q_ASSERT(up || key == Qt::Key_Down);
const QDate startDate(2000, up ? 2 : 12, 17);
for (auto modifier : modifierList) {
QTestEventList keys;
keys.addKeyClick(key, modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
for (const auto section : sections) {
const auto expectedDate = stepDate(startDate, section, steps);
if (!expectedDate.isValid())
continue;
const auto sectionName = sectionToName(section);
QTest::addRow("%s%sWith%sKeyboardModifier",
up ? "up" : "down",
sectionName.latin1(),
modifierName.latin1())
<< startDate
<< section
<< keys
<< expectedDate;
}
}
}
}
void tst_QDateTimeEdit::stepModifierKeys()
{
QFETCH(QDate, startDate);
QFETCH(QDateTimeEdit::Section, section);
QFETCH(QTestEventList, keys);
QFETCH(QDate, expectedDate);
QDateTimeEdit edit(0);
edit.setDate(startDate);
edit.show();
QVERIFY(QTest::qWaitForWindowActive(&edit));
edit.setCurrentSection(section);
QCOMPARE(edit.date(), startDate);
keys.simulate(&edit);
QCOMPARE(edit.date(), expectedDate);
}
void tst_QDateTimeEdit::stepModifierButtons_data()
{
QTest::addColumn<QStyle::SubControl>("subControl");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<QDateTimeEdit::Section>("section");
QTest::addColumn<QTime>("startTime");
QTest::addColumn<QTime>("expectedTime");
const auto subControls = {QStyle::SC_SpinBoxUp, QStyle::SC_SpinBoxDown};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
const auto sections = {QDateTimeEdit::SecondSection,
QDateTimeEdit::MinuteSection,
QDateTimeEdit::HourSection};
const QTime startTime(12, 36, 24);
for (auto subControl : subControls) {
const bool up = subControl == QStyle::SC_SpinBoxUp;
Q_ASSERT(up || subControl == QStyle::SC_SpinBoxDown);
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
for (const auto section : sections) {
const auto expectedTime = stepTime(startTime, section, steps);
if (!expectedTime.isValid())
continue;
const auto sectionName = sectionToName(section);
QTest::addRow("%s%sWith%sKeyboardModifier",
up ? "up" : "down",
sectionName.latin1(),
modifierName.latin1())
<< subControl
<< modifiers
<< section
<< startTime
<< expectedTime;
}
}
}
}
void tst_QDateTimeEdit::stepModifierButtons()
{
QFETCH(QStyle::SubControl, subControl);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(QDateTimeEdit::Section, section);
QFETCH(QTime, startTime);
QFETCH(QTime, expectedTime);
EditorDateEdit edit(0);
edit.setTime(startTime);
edit.show();
QVERIFY(QTest::qWaitForWindowActive(&edit));
edit.setCurrentSection(section);
QStyleOptionSpinBox spinBoxStyleOption;
edit.initStyleOption(&spinBoxStyleOption);
const QRect buttonRect = edit.style()->subControlRect(
QStyle::CC_SpinBox, &spinBoxStyleOption, subControl, &edit);
QCOMPARE(edit.time(), startTime);
QTest::mouseClick(&edit, Qt::LeftButton, modifiers, buttonRect.center());
QCOMPARE(edit.time(), expectedTime);
}
void tst_QDateTimeEdit::stepModifierPressAndHold_data()
{
QTest::addColumn<QStyle::SubControl>("subControl");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<int>("expectedStepModifier");
const auto subControls = {QStyle::SC_SpinBoxUp, QStyle::SC_SpinBoxDown};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
for (auto subControl : subControls) {
const bool up = subControl == QStyle::SC_SpinBoxUp;
Q_ASSERT(up || subControl == QStyle::SC_SpinBoxDown);
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
QTest::addRow("%sWith%sKeyboardModifier",
up ? "up" : "down",
modifierName.latin1())
<< subControl
<< modifiers
<< steps;
}
}
}
void tst_QDateTimeEdit::stepModifierPressAndHold()
{
QFETCH(QStyle::SubControl, subControl);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(int, expectedStepModifier);
const QDate startDate(2000, 1, 1);
EditorDateEdit edit(0);
QScopedPointer<PressAndHoldStyle, QScopedPointerDeleteLater> pressAndHoldStyle(
new PressAndHoldStyle);
edit.setStyle(pressAndHoldStyle.data());
edit.setDate(startDate);
QSignalSpy spy(&edit, &EditorDateEdit::dateChanged);
edit.show();
QVERIFY(QTest::qWaitForWindowActive(&edit));
edit.setCurrentSection(QDateTimeEdit::YearSection);
QStyleOptionSpinBox spinBoxStyleOption;
edit.initStyleOption(&spinBoxStyleOption);
const QRect buttonRect = edit.style()->subControlRect(
QStyle::CC_SpinBox, &spinBoxStyleOption, subControl, &edit);
QTest::mousePress(&edit, Qt::LeftButton, modifiers, buttonRect.center());
QTRY_VERIFY(spy.length() >= 3);
QTest::mouseRelease(&edit, Qt::LeftButton, modifiers, buttonRect.center());
const auto value = spy.last().at(0);
QVERIFY(value.type() == QVariant::Date);
const QDate expectedDate = startDate.addYears(spy.length() *
expectedStepModifier);
QCOMPARE(value.toDate(), expectedDate);
}
QTEST_MAIN(tst_QDateTimeEdit)
#include "tst_qdatetimeedit.moc"

View File

@ -41,6 +41,10 @@
#include <qlineedit.h>
#include <qdebug.h>
#include <QStyleOptionSpinBox>
#include <QStyle>
#include <QProxyStyle>
class DoubleSpinBox : public QDoubleSpinBox
{
Q_OBJECT
@ -60,6 +64,16 @@ public:
{
return QDoubleSpinBox::valueFromText(text);
}
#if QT_CONFIG(wheelevent)
void wheelEvent(QWheelEvent *event)
{
QDoubleSpinBox::wheelEvent(event);
}
#endif
void initStyleOption(QStyleOptionSpinBox *option) const
{
QDoubleSpinBox::initStyleOption(option);
}
QLineEdit* lineEdit() const { return QDoubleSpinBox::lineEdit(); }
public slots:
@ -69,6 +83,26 @@ public slots:
}
};
class PressAndHoldStyle : public QProxyStyle
{
Q_OBJECT
public:
using QProxyStyle::QProxyStyle;
int styleHint(QStyle::StyleHint hint, const QStyleOption *option = nullptr,
const QWidget *widget = nullptr, QStyleHintReturn *returnData = nullptr) const override
{
switch (hint) {
case QStyle::SH_SpinBox_ClickAutoRepeatRate:
return 5;
case QStyle::SH_SpinBox_ClickAutoRepeatThreshold:
return 10;
default:
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
}
};
class tst_QDoubleSpinBox : public QObject
{
@ -138,6 +172,17 @@ private slots:
void adaptiveDecimalStep();
void wheelEvents_data();
void wheelEvents();
void stepModifierKeys_data();
void stepModifierKeys();
void stepModifierButtons_data();
void stepModifierButtons();
void stepModifierPressAndHold_data();
void stepModifierPressAndHold();
public slots:
void valueChangedHelper(const QString &);
void valueChangedHelper(double);
@ -152,6 +197,24 @@ typedef QList<double> DoubleList;
Q_DECLARE_METATYPE(QLocale::Language)
Q_DECLARE_METATYPE(QLocale::Country)
static QLatin1String modifierToName(Qt::KeyboardModifier modifier)
{
switch (modifier) {
case Qt::NoModifier:
return QLatin1Literal("No");
break;
case Qt::ControlModifier:
return QLatin1Literal("Ctrl");
break;
case Qt::ShiftModifier:
return QLatin1Literal("Shift");
break;
default:
qFatal("Unexpected keyboard modifier");
return QLatin1String();
}
}
tst_QDoubleSpinBox::tst_QDoubleSpinBox()
{
@ -1270,5 +1333,332 @@ void tst_QDoubleSpinBox::adaptiveDecimalStep()
}
}
void tst_QDoubleSpinBox::wheelEvents_data()
{
#if QT_CONFIG(wheelevent)
QTest::addColumn<QPoint>("angleDelta");
QTest::addColumn<int>("qt4Delta");
QTest::addColumn<Qt::KeyboardModifiers>("modifier");
QTest::addColumn<Qt::MouseEventSource>("source");
QTest::addColumn<double>("start");
QTest::addColumn<DoubleList>("expectedValues");
const auto fractions = {false, true};
const auto directions = {true, false};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
const auto sources = {Qt::MouseEventNotSynthesized,
Qt::MouseEventSynthesizedBySystem,
Qt::MouseEventSynthesizedByQt,
Qt::MouseEventSynthesizedByApplication};
const double startValue = 0.0;
for (auto fraction : fractions) {
for (auto up : directions) {
const int units = (fraction ? 60 : 120) * (up ? 1 : -1);
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
for (auto source : sources) {
#ifdef Q_OS_MACOS
QPoint angleDelta;
if ((modifier & Qt::ShiftModifier) &&
source == Qt::MouseEventNotSynthesized) {
// On macOS the Shift modifier converts vertical
// mouse wheel events to horizontal.
angleDelta = { units, 0 };
} else {
// However, this is not the case for trackpad scroll
// events.
angleDelta = { 0, units };
}
#else
const QPoint angleDelta(0, units);
#endif
QLatin1String sourceName;
switch (source) {
case Qt::MouseEventNotSynthesized:
sourceName = QLatin1Literal("NotSynthesized");
break;
case Qt::MouseEventSynthesizedBySystem:
sourceName = QLatin1Literal("SynthesizedBySystem");
break;
case Qt::MouseEventSynthesizedByQt:
sourceName = QLatin1Literal("SynthesizedByQt");
break;
case Qt::MouseEventSynthesizedByApplication:
sourceName = QLatin1Literal("SynthesizedByApplication");
break;
default:
qFatal("Unexpected wheel event source");
continue;
}
DoubleList expectedValues;
if (fraction)
expectedValues << startValue;
expectedValues << startValue + steps;
QTest::addRow("%s%sWith%sKeyboardModifier%s",
fraction ? "half" : "full",
up ? "Up" : "Down",
modifierName.latin1(),
sourceName.latin1())
<< angleDelta
<< units
<< modifiers
<< source
<< startValue
<< expectedValues;
}
}
}
}
#else
QSKIP("Built with --no-feature-wheelevent");
#endif
}
void tst_QDoubleSpinBox::wheelEvents()
{
#if QT_CONFIG(wheelevent)
QFETCH(QPoint, angleDelta);
QFETCH(int, qt4Delta);
QFETCH(Qt::KeyboardModifiers, modifier);
QFETCH(Qt::MouseEventSource, source);
QFETCH(double, start);
QFETCH(DoubleList, expectedValues);
DoubleSpinBox spinBox;
spinBox.setRange(-20, 20);
spinBox.setValue(start);
QWheelEvent event(QPointF(), QPointF(), QPoint(), angleDelta, qt4Delta,
Qt::Vertical, Qt::NoButton, modifier, Qt::NoScrollPhase,
source);
for (int expected : expectedValues) {
qApp->sendEvent(&spinBox, &event);
QCOMPARE(spinBox.value(), expected);
}
#else
QSKIP("Built with --no-feature-wheelevent");
#endif
}
void tst_QDoubleSpinBox::stepModifierKeys_data()
{
QTest::addColumn<double>("startValue");
QTest::addColumn<QTestEventList>("keys");
QTest::addColumn<double>("expectedValue");
const auto keyList = {Qt::Key_Up, Qt::Key_Down};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
for (auto key : keyList) {
const bool up = key == Qt::Key_Up;
Q_ASSERT(up || key == Qt::Key_Down);
const double startValue = up ? 0.0 : 10.0;
for (auto modifier : modifierList) {
QTestEventList keys;
keys.addKeyClick(key, modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
const double expectedValue = startValue + steps;
QTest::addRow("%sWith%sKeyboardModifier",
up ? "up" : "down",
modifierName.latin1())
<< startValue
<< keys
<< expectedValue;
}
}
}
void tst_QDoubleSpinBox::stepModifierKeys()
{
QFETCH(double, startValue);
QFETCH(QTestEventList, keys);
QFETCH(double, expectedValue);
QDoubleSpinBox spin(0);
spin.setValue(startValue);
spin.show();
QVERIFY(QTest::qWaitForWindowActive(&spin));
QCOMPARE(spin.value(), startValue);
keys.simulate(&spin);
QCOMPARE(spin.value(), expectedValue);
}
void tst_QDoubleSpinBox::stepModifierButtons_data()
{
QTest::addColumn<QStyle::SubControl>("subControl");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<double>("startValue");
QTest::addColumn<double>("expectedValue");
const auto subControls = {QStyle::SC_SpinBoxUp, QStyle::SC_SpinBoxDown};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
for (auto subControl : subControls) {
const bool up = subControl == QStyle::SC_SpinBoxUp;
Q_ASSERT(up || subControl == QStyle::SC_SpinBoxDown);
const double startValue = up ? 0 : 10;
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
const double expectedValue = startValue + steps;
QTest::addRow("%sWith%sKeyboardModifier",
up ? "up" : "down",
modifierName.latin1())
<< subControl
<< modifiers
<< startValue
<< expectedValue;
}
}
}
void tst_QDoubleSpinBox::stepModifierButtons()
{
QFETCH(QStyle::SubControl, subControl);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(double, startValue);
QFETCH(double, expectedValue);
DoubleSpinBox spin(0);
spin.setRange(-20, 20);
spin.setValue(startValue);
spin.show();
QVERIFY(QTest::qWaitForWindowActive(&spin));
QStyleOptionSpinBox spinBoxStyleOption;
spin.initStyleOption(&spinBoxStyleOption);
const QRect buttonRect = spin.style()->subControlRect(
QStyle::CC_SpinBox, &spinBoxStyleOption, subControl, &spin);
QCOMPARE(spin.value(), startValue);
QTest::mouseClick(&spin, Qt::LeftButton, modifiers, buttonRect.center());
QCOMPARE(spin.value(), expectedValue);
}
void tst_QDoubleSpinBox::stepModifierPressAndHold_data()
{
QTest::addColumn<QStyle::SubControl>("subControl");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<int>("expectedStepModifier");
const auto subControls = {QStyle::SC_SpinBoxUp, QStyle::SC_SpinBoxDown};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
for (auto subControl : subControls) {
const bool up = subControl == QStyle::SC_SpinBoxUp;
Q_ASSERT(up || subControl == QStyle::SC_SpinBoxDown);
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
QTest::addRow("%sWith%sKeyboardModifier",
up ? "up" : "down",
modifierName.latin1())
<< subControl
<< modifiers
<< steps;
}
}
}
void tst_QDoubleSpinBox::stepModifierPressAndHold()
{
QFETCH(QStyle::SubControl, subControl);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(int, expectedStepModifier);
DoubleSpinBox spin(0);
QScopedPointer<PressAndHoldStyle, QScopedPointerDeleteLater> pressAndHoldStyle(
new PressAndHoldStyle);
spin.setStyle(pressAndHoldStyle.data());
spin.setRange(-100.0, 100.0);
spin.setValue(0.0);
QSignalSpy spy(&spin, QOverload<double>::of(&DoubleSpinBox::valueChanged));
spin.show();
QVERIFY(QTest::qWaitForWindowExposed(&spin));
QStyleOptionSpinBox spinBoxStyleOption;
spin.initStyleOption(&spinBoxStyleOption);
const QRect buttonRect = spin.style()->subControlRect(
QStyle::CC_SpinBox, &spinBoxStyleOption, subControl, &spin);
QTest::mousePress(&spin, Qt::LeftButton, modifiers, buttonRect.center());
QTRY_VERIFY(spy.length() >= 3);
QTest::mouseRelease(&spin, Qt::LeftButton, modifiers, buttonRect.center());
const auto value = spy.last().at(0);
QVERIFY(value.type() == QVariant::Double);
QCOMPARE(value.toDouble(), spy.length() * expectedStepModifier);
}
QTEST_MAIN(tst_QDoubleSpinBox)
#include "tst_qdoublespinbox.moc"

View File

@ -50,6 +50,9 @@
#include <QKeySequence>
#include <QStackedWidget>
#include <QDebug>
#include <QStyleOptionSpinBox>
#include <QStyle>
#include <QProxyStyle>
class SpinBox : public QSpinBox
{
@ -75,10 +78,34 @@ public:
QSpinBox::wheelEvent(event);
}
#endif
void initStyleOption(QStyleOptionSpinBox *option) const
{
QSpinBox::initStyleOption(option);
}
QLineEdit *lineEdit() const { return QSpinBox::lineEdit(); }
};
class PressAndHoldStyle : public QProxyStyle
{
Q_OBJECT
public:
using QProxyStyle::QProxyStyle;
int styleHint(QStyle::StyleHint hint, const QStyleOption *option = nullptr,
const QWidget *widget = nullptr, QStyleHintReturn *returnData = nullptr) const override
{
switch (hint) {
case QStyle::SH_SpinBox_ClickAutoRepeatRate:
return 5;
case QStyle::SH_SpinBox_ClickAutoRepeatThreshold:
return 10;
default:
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
}
};
class tst_QSpinBox : public QObject
{
Q_OBJECT
@ -143,10 +170,19 @@ private slots:
void setGroupSeparatorShown_data();
void setGroupSeparatorShown();
void wheelEvents_data();
void wheelEvents();
void adaptiveDecimalStep();
void stepModifierKeys_data();
void stepModifierKeys();
void stepModifierButtons_data();
void stepModifierButtons();
void stepModifierPressAndHold_data();
void stepModifierPressAndHold();
public slots:
void valueChangedHelper(const QString &);
void valueChangedHelper(int);
@ -160,6 +196,24 @@ typedef QList<int> IntList;
Q_DECLARE_METATYPE(QLocale::Language)
Q_DECLARE_METATYPE(QLocale::Country)
static QLatin1String modifierToName(Qt::KeyboardModifier modifier)
{
switch (modifier) {
case Qt::NoModifier:
return QLatin1Literal("No");
break;
case Qt::ControlModifier:
return QLatin1Literal("Ctrl");
break;
case Qt::ShiftModifier:
return QLatin1Literal("Shift");
break;
default:
qFatal("Unexpected keyboard modifier");
return QLatin1String();
}
}
// Testing get/set functions
void tst_QSpinBox::getSetCheck()
{
@ -1217,27 +1271,132 @@ void tst_QSpinBox::setGroupSeparatorShown()
QCOMPARE(spinBox.value()+1000, 33000);
}
void tst_QSpinBox::wheelEvents_data()
{
#if QT_CONFIG(wheelevent)
QTest::addColumn<QPoint>("angleDelta");
QTest::addColumn<int>("qt4Delta");
QTest::addColumn<Qt::KeyboardModifiers>("modifier");
QTest::addColumn<Qt::MouseEventSource>("source");
QTest::addColumn<int>("start");
QTest::addColumn<IntList>("expectedValues");
const auto fractions = {false, true};
const auto directions = {true, false};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
const auto sources = {Qt::MouseEventNotSynthesized,
Qt::MouseEventSynthesizedBySystem,
Qt::MouseEventSynthesizedByQt,
Qt::MouseEventSynthesizedByApplication};
const int startValue = 0;
for (auto fraction : fractions) {
for (auto up : directions) {
const int units = (fraction ? 60 : 120) * (up ? 1 : -1);
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
for (auto source : sources) {
#ifdef Q_OS_MACOS
QPoint angleDelta;
if ((modifier & Qt::ShiftModifier) &&
source == Qt::MouseEventNotSynthesized) {
// On macOS the Shift modifier converts vertical
// mouse wheel events to horizontal.
angleDelta = { units, 0 };
} else {
// However, this is not the case for trackpad scroll
// events.
angleDelta = { 0, units };
}
#else
const QPoint angleDelta(0, units);
#endif
QLatin1String sourceName;
switch (source) {
case Qt::MouseEventNotSynthesized:
sourceName = QLatin1Literal("NotSynthesized");
break;
case Qt::MouseEventSynthesizedBySystem:
sourceName = QLatin1Literal("SynthesizedBySystem");
break;
case Qt::MouseEventSynthesizedByQt:
sourceName = QLatin1Literal("SynthesizedByQt");
break;
case Qt::MouseEventSynthesizedByApplication:
sourceName = QLatin1Literal("SynthesizedByApplication");
break;
default:
qFatal("Unexpected wheel event source");
continue;
}
IntList expectedValues;
if (fraction)
expectedValues << startValue;
expectedValues << startValue + steps;
QTest::addRow("%s%sWith%sKeyboardModifier%s",
fraction ? "half" : "full",
up ? "Up" : "Down",
modifierName.latin1(),
sourceName.latin1())
<< angleDelta
<< units
<< modifiers
<< source
<< startValue
<< expectedValues;
}
}
}
}
#else
QSKIP("Built with --no-feature-wheelevent");
#endif
}
void tst_QSpinBox::wheelEvents()
{
#if QT_CONFIG(wheelevent)
QFETCH(QPoint, angleDelta);
QFETCH(int, qt4Delta);
QFETCH(Qt::KeyboardModifiers, modifier);
QFETCH(Qt::MouseEventSource, source);
QFETCH(int, start);
QFETCH(IntList, expectedValues);
SpinBox spinBox;
spinBox.setRange(-20, 20);
spinBox.setValue(0);
spinBox.setValue(start);
QWheelEvent wheelUp(QPointF(), QPointF(), QPoint(), QPoint(0, 120), 120, Qt::Vertical, Qt::NoButton, Qt::NoModifier);
spinBox.wheelEvent(&wheelUp);
QCOMPARE(spinBox.value(), 1);
QWheelEvent wheelDown(QPointF(), QPointF(), QPoint(), QPoint(0, -120), -120, Qt::Vertical, Qt::NoButton, Qt::NoModifier);
spinBox.wheelEvent(&wheelDown);
spinBox.wheelEvent(&wheelDown);
QCOMPARE(spinBox.value(), -1);
QWheelEvent wheelHalfUp(QPointF(), QPointF(), QPoint(), QPoint(0, 60), 60, Qt::Vertical, Qt::NoButton, Qt::NoModifier);
spinBox.wheelEvent(&wheelHalfUp);
QCOMPARE(spinBox.value(), -1);
spinBox.wheelEvent(&wheelHalfUp);
QCOMPARE(spinBox.value(), 0);
QWheelEvent event(QPointF(), QPointF(), QPoint(), angleDelta, qt4Delta,
Qt::Vertical, Qt::NoButton, modifier, Qt::NoScrollPhase,
source);
for (int expected : expectedValues) {
qApp->sendEvent(&spinBox, &event);
QCOMPARE(spinBox.value(), expected);
}
#else
QSKIP("Built with --no-feature-wheelevent");
#endif
}
@ -1320,5 +1479,203 @@ void tst_QSpinBox::adaptiveDecimalStep()
}
}
void tst_QSpinBox::stepModifierKeys_data()
{
QTest::addColumn<int>("startValue");
QTest::addColumn<QTestEventList>("keys");
QTest::addColumn<int>("expectedValue");
const auto keyList = {Qt::Key_Up, Qt::Key_Down};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
for (auto key : keyList) {
const bool up = key == Qt::Key_Up;
Q_ASSERT(up || key == Qt::Key_Down);
const int startValue = up ? 0.0 : 10.0;
for (auto modifier : modifierList) {
QTestEventList keys;
keys.addKeyClick(key, modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
const int expectedValue = startValue + steps;
QTest::addRow("%sWith%sKeyboardModifier",
up ? "up" : "down",
modifierName.latin1())
<< startValue
<< keys
<< expectedValue;
}
}
}
void tst_QSpinBox::stepModifierKeys()
{
QFETCH(int, startValue);
QFETCH(QTestEventList, keys);
QFETCH(int, expectedValue);
QSpinBox spin(0);
spin.setValue(startValue);
spin.show();
QVERIFY(QTest::qWaitForWindowActive(&spin));
QCOMPARE(spin.value(), startValue);
keys.simulate(&spin);
QCOMPARE(spin.value(), expectedValue);
}
void tst_QSpinBox::stepModifierButtons_data()
{
QTest::addColumn<QStyle::SubControl>("subControl");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<int>("startValue");
QTest::addColumn<int>("expectedValue");
const auto subControls = {QStyle::SC_SpinBoxUp, QStyle::SC_SpinBoxDown};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
for (auto subControl : subControls) {
const bool up = subControl == QStyle::SC_SpinBoxUp;
Q_ASSERT(up || subControl == QStyle::SC_SpinBoxDown);
const int startValue = up ? 0 : 10;
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
const int expectedValue = startValue + steps;
QTest::addRow("%sWith%sKeyboardModifier",
up ? "up" : "down",
modifierName.latin1())
<< subControl
<< modifiers
<< startValue
<< expectedValue;
}
}
}
void tst_QSpinBox::stepModifierButtons()
{
QFETCH(QStyle::SubControl, subControl);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(int, startValue);
QFETCH(int, expectedValue);
SpinBox spin(0);
spin.setRange(-20, 20);
spin.setValue(startValue);
spin.show();
QVERIFY(QTest::qWaitForWindowActive(&spin));
QStyleOptionSpinBox spinBoxStyleOption;
spin.initStyleOption(&spinBoxStyleOption);
const QRect buttonRect = spin.style()->subControlRect(
QStyle::CC_SpinBox, &spinBoxStyleOption, subControl, &spin);
QCOMPARE(spin.value(), startValue);
QTest::mouseClick(&spin, Qt::LeftButton, modifiers, buttonRect.center());
QCOMPARE(spin.value(), expectedValue);
}
void tst_QSpinBox::stepModifierPressAndHold_data()
{
QTest::addColumn<QStyle::SubControl>("subControl");
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<int>("expectedStepModifier");
const auto subControls = {QStyle::SC_SpinBoxUp, QStyle::SC_SpinBoxDown};
const auto modifierList = {Qt::NoModifier,
Qt::ControlModifier,
Qt::ShiftModifier};
for (auto subControl : subControls) {
const bool up = subControl == QStyle::SC_SpinBoxUp;
Q_ASSERT(up || subControl == QStyle::SC_SpinBoxDown);
for (auto modifier : modifierList) {
const Qt::KeyboardModifiers modifiers(modifier);
const auto modifierName = modifierToName(modifier);
if (modifierName.isEmpty())
continue;
const int steps = (modifier & Qt::ControlModifier ? 10 : 1)
* (up ? 1 : -1);
QTest::addRow("%sWith%sKeyboardModifier",
up ? "up" : "down",
modifierName.latin1())
<< subControl
<< modifiers
<< steps;
}
}
}
void tst_QSpinBox::stepModifierPressAndHold()
{
QFETCH(QStyle::SubControl, subControl);
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(int, expectedStepModifier);
SpinBox spin(0);
QScopedPointer<PressAndHoldStyle, QScopedPointerDeleteLater> pressAndHoldStyle(
new PressAndHoldStyle);
spin.setStyle(pressAndHoldStyle.data());
spin.setRange(-100, 100);
spin.setValue(0);
QSignalSpy spy(&spin, QOverload<int>::of(&SpinBox::valueChanged));
spin.show();
QVERIFY(QTest::qWaitForWindowActive(&spin));
QStyleOptionSpinBox spinBoxStyleOption;
spin.initStyleOption(&spinBoxStyleOption);
const QRect buttonRect = spin.style()->subControlRect(
QStyle::CC_SpinBox, &spinBoxStyleOption, subControl, &spin);
QTest::mousePress(&spin, Qt::LeftButton, modifiers, buttonRect.center());
QTRY_VERIFY(spy.length() >= 3);
QTest::mouseRelease(&spin, Qt::LeftButton, modifiers, buttonRect.center());
const auto value = spy.last().at(0);
QVERIFY(value.type() == QVariant::Int);
QCOMPARE(value.toInt(), spy.length() * expectedStepModifier);
}
QTEST_MAIN(tst_QSpinBox)
#include "tst_qspinbox.moc"