diff --git a/src/widgets/widgets/qtabbar.cpp b/src/widgets/widgets/qtabbar.cpp index 8d21e7d257..3fdd6fc314 100644 --- a/src/widgets/widgets/qtabbar.cpp +++ b/src/widgets/widgets/qtabbar.cpp @@ -2388,10 +2388,48 @@ void QTabBar::wheelEvent(QWheelEvent *event) { Q_D(QTabBar); if (style()->styleHint(QStyle::SH_TabBar_AllowWheelScrolling)) { - int delta = (qAbs(event->angleDelta().x()) > qAbs(event->angleDelta().y()) ? - event->angleDelta().x() : event->angleDelta().y()); - int offset = delta > 0 ? -1 : 1; - d->setCurrentNextEnabledIndex(offset); + const bool wheelVertical = qAbs(event->angleDelta().y()) > qAbs(event->angleDelta().x()); + const bool tabsVertical = verticalTabs(d->shape); + if (event->device()->capabilities().testFlag(QInputDevice::Capability::PixelScroll)) { + // For wheels/touch pads with pixel precision, scroll the tab bar if + // it has the right orientation. + int delta = 0; + if (tabsVertical == wheelVertical) + delta = wheelVertical ? event->pixelDelta().y() : event->pixelDelta().x(); + if (layoutDirection() == Qt::RightToLeft) + delta = -delta; + if (delta && d->validIndex(d->lastVisible)) { + const int oldScrollOffset = d->scrollOffset; + const QRect lastTabRect = d->tabList.at(d->lastVisible)->rect; + const QRect scrollRect = d->normalizedScrollRect(d->lastVisible); + int scrollRectExtent = scrollRect.right(); + if (!d->leftB->isVisible()) + scrollRectExtent += tabsVertical ? d->leftB->height() : d->leftB->width(); + if (!d->rightB->isVisible()) + scrollRectExtent += tabsVertical ? d->rightB->height() : d->rightB->width(); + + const int maxScrollOffset = (tabsVertical ? lastTabRect.bottom() + : lastTabRect.right()) + - scrollRectExtent; + d->scrollOffset = qBound(0, d->scrollOffset - delta, maxScrollOffset); + d->leftB->setEnabled(d->scrollOffset > -scrollRect.left()); + d->rightB->setEnabled(maxScrollOffset > d->scrollOffset); + if (oldScrollOffset != d->scrollOffset) { + event->accept(); + update(); + return; + } + } + } else { + const int delta = wheelVertical ? event->angleDelta().y() : event->angleDelta().x(); + const int offset = delta > 0 ? -1 : 1; + const int oldCurrentIndex = d->currentIndex; + d->setCurrentNextEnabledIndex(offset); + if (oldCurrentIndex != d->currentIndex) { + event->accept(); + return; + } + } QWidget::wheelEvent(event); } } diff --git a/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp b/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp index 41f1cb5e64..630b5c1167 100644 --- a/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp +++ b/tests/auto/widgets/widgets/qtabbar/tst_qtabbar.cpp @@ -101,6 +101,8 @@ private slots: void mouseReleaseOutsideTabBar(); void mouseWheel(); + void kineticWheel_data(); + void kineticWheel(); void scrollButtons_data(); void scrollButtons(); @@ -889,20 +891,25 @@ void tst_QTabBar::checkPositions(const TabBar &tabbar, const QList &positio } #if QT_CONFIG(wheelevent) -// defined to be 120 by the wheel mouse vendors according to the docs -#define WHEEL_DELTA 120 class TabBarScrollingProxyStyle : public QProxyStyle { public: - TabBarScrollingProxyStyle() : QProxyStyle(), scrolling(true) + TabBarScrollingProxyStyle(const QString &defStyle = {}) + : QProxyStyle(defStyle), scrolling(true) { } int styleHint(StyleHint hint, const QStyleOption *option = 0, const QWidget *widget = 0, QStyleHintReturn *returnData = 0) const override { - if (hint == QStyle::SH_TabBar_AllowWheelScrolling) + switch (hint) { + case QStyle::SH_TabBar_AllowWheelScrolling: return scrolling; + case SH_TabBar_ElideMode: + return Qt::ElideNone; + default: + break; + } return QProxyStyle::styleHint(hint, option, widget, returnData); } @@ -912,37 +919,193 @@ public: void tst_QTabBar::mouseWheel() { + TabBar tabbar; - // apply custom style to app, which can toggle tabbar scrolling behavior - QCoreApplication *applicationInstance = QApplication::instance(); - QVERIFY(applicationInstance != 0); - auto *proxyStyle = new TabBarScrollingProxyStyle; - QApplication::setStyle(proxyStyle); + // apply custom style to the tabbar, which can toggle tabbar scrolling behavior + TabBarScrollingProxyStyle proxyStyle; + tabbar.setStyle(&proxyStyle); // make tabbar with three tabs, select the middle one - TabBar tabbar; tabbar.addTab("one"); tabbar.addTab("two"); tabbar.addTab("three"); int startIndex = 1; tabbar.setCurrentIndex(startIndex); + const auto systemId = QPointingDevice::primaryPointingDevice()->systemId() + 1; + QPointingDevice clickyWheel("test clicky wheel", systemId, QInputDevice::DeviceType::Mouse, + QPointingDevice::PointerType::Generic, + QInputDevice::Capability::Position | QInputDevice::Capability::Scroll, + 1, 3); + // define scroll event const QPoint wheelPoint = tabbar.rect().bottomRight(); - QWheelEvent event(wheelPoint, tabbar.mapToGlobal(wheelPoint), QPoint(), QPoint(0, WHEEL_DELTA), - Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false); + QWheelEvent event(wheelPoint, tabbar.mapToGlobal(wheelPoint), + QPoint(), QPoint(0, QWheelEvent::DefaultDeltasPerStep), + Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, + Qt::MouseEventSynthesizedByApplication, &clickyWheel); // disable scrolling, send scroll event, confirm that tab did not change - proxyStyle->scrolling = false; - QVERIFY(applicationInstance->sendEvent(&tabbar, &event)); + proxyStyle.scrolling = false; + QVERIFY(QApplication::sendEvent(&tabbar, &event)); QVERIFY(tabbar.currentIndex() == startIndex); // enable scrolling, send scroll event, confirm that tab changed - proxyStyle->scrolling = true; - QVERIFY(applicationInstance->sendEvent(&tabbar, &event)); + proxyStyle.scrolling = true; + QVERIFY(QApplication::sendEvent(&tabbar, &event)); QVERIFY(tabbar.currentIndex() != startIndex); } +void tst_QTabBar::kineticWheel_data() +{ + QTest::addColumn("tabShape"); + + QTest::addRow("North") << QTabBar::RoundedNorth; + QTest::addRow("East") << QTabBar::RoundedEast; + QTest::addRow("South") << QTabBar::RoundedSouth; + QTest::addRow("West") << QTabBar::RoundedWest; +} + +void tst_QTabBar::kineticWheel() +{ + const auto systemId = QPointingDevice::primaryPointingDevice()->systemId() + 1; + QPointingDevice pixelPad("test pixel pad", systemId, QInputDevice::DeviceType::TouchPad, + QPointingDevice::PointerType::Generic, + QInputDevice::Capability::Position | QInputDevice::Capability::PixelScroll, + 1, 3); + + QFETCH(QTabBar::Shape, tabShape); + QWidget window; + TabBar tabbar(&window); + // Since the macOS style makes sure that all tabs are always visible, we + // replace it with the windows style for this test, and use the proxy that + // makes sure that scrolling is enabled and that tab texts are not elided. + QString defaultStyle; + if (QApplication::style()->name() == QStringLiteral("macos")) + defaultStyle = "windows"; + TabBarScrollingProxyStyle proxyStyle(defaultStyle); + tabbar.setStyle(&proxyStyle); + + tabbar.addTab("long tab text 1"); + tabbar.addTab("long tab text 2"); + tabbar.addTab("long tab text 3"); + + // Make sure we don't have enough space for the tabs and need to scroll + const int tabbarLength = tabbar.tabRect(0).width() * 2; + + tabbar.setShape(tabShape); + const bool horizontal = tabShape == QTabBar::RoundedNorth + || tabShape == QTabBar::RoundedSouth; + if (horizontal) + tabbar.setFixedWidth(tabbarLength); + else + tabbar.setFixedHeight(tabbarLength); + + // start with the middle tab, QTabBar will scroll to make it visible + const int startIndex = 1; + tabbar.setCurrentIndex(startIndex); + + window.setMinimumSize(tabbarLength, tabbarLength); + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + + const auto *leftButton = tabbar.findChild(u"ScrollLeftButton"_qs); + const auto *rightButton = tabbar.findChild(u"ScrollRightButton"_qs); + QVERIFY(leftButton && rightButton); + QVERIFY(leftButton->isEnabled() && rightButton->isEnabled()); + + // Figure out if any of the buttons is laid out to be in front of the tabs. + // We can't use setUsesScrollButtons(false), as then several styles will enforce + // a minimum size for the tab bar. + const bool leftInFront = ((horizontal && leftButton->pos().x() < tabbar.rect().center().x()) + || (!horizontal && leftButton->pos().y() < tabbar.rect().center().y())); + const bool rightInFront = ((horizontal && rightButton->pos().x() < tabbar.rect().center().x()) + || (!horizontal && rightButton->pos().y() < tabbar.rect().center().y())); + QPoint leftEdge; + QPoint rightEdge; + if (leftInFront && rightInFront) { // both on the left + leftEdge = rightButton->geometry().bottomRight(); + rightEdge = tabbar.rect().bottomRight(); + } else if (leftInFront && !rightInFront) { + leftEdge = leftButton->geometry().bottomRight(); + rightEdge = rightButton->geometry().topLeft(); + } else { // both on the right + leftEdge = QPoint(0, 0); + rightEdge = leftButton->geometry().topLeft(); + } + // avoid border lines + leftEdge += QPoint(2, 2); + if (horizontal) { + rightEdge += QPoint(-2, 2); + } else { + rightEdge += QPoint(2, -2); + } + + QCOMPARE(tabbar.tabAt(leftEdge), 0); + QCOMPARE(tabbar.tabAt(rightEdge), 1); + + const QPoint delta = horizontal ? QPoint(10, 0) : QPoint(0, 10); + const QPoint wheelPoint = tabbar.rect().center(); + + bool accepted = true; + Qt::ScrollPhase phase = Qt::ScrollBegin; + // scroll all the way to the end + while (accepted) { + QWheelEvent event(wheelPoint, tabbar.mapToGlobal(wheelPoint), -delta, -delta, + Qt::NoButton, Qt::NoModifier, phase, false, + Qt::MouseEventSynthesizedByApplication, &pixelPad); + if (phase == Qt::ScrollBegin) + phase = Qt::ScrollUpdate; + QApplication::sendEvent(&tabbar, &event); + accepted = event.isAccepted(); + } + QCOMPARE(tabbar.tabAt(leftEdge), 1); + QCOMPARE(tabbar.tabAt(rightEdge), 2); + QVERIFY(leftButton->isEnabled()); + QVERIFY(!rightButton->isEnabled()); + // kinetic wheel events don't change the current index + QVERIFY(tabbar.currentIndex() == startIndex); + + // scroll all the way to the beginning + accepted = true; + while (accepted) { + QWheelEvent event(wheelPoint, tabbar.mapToGlobal(wheelPoint), delta, delta, + Qt::NoButton, Qt::NoModifier, phase, false, + Qt::MouseEventSynthesizedByApplication, &pixelPad); + QApplication::sendEvent(&tabbar, &event); + accepted = event.isAccepted(); + } + + QCOMPARE(tabbar.tabAt(leftEdge), 0); + QCOMPARE(tabbar.tabAt(rightEdge), 1); + QVERIFY(!leftButton->isEnabled()); + QVERIFY(rightButton->isEnabled()); + // kinetic wheel events don't change the current index + QVERIFY(tabbar.currentIndex() == startIndex); + + // make tabs small so that we have enough space, and verify sure we can't scroll + tabbar.setTabText(0, "A"); + tabbar.setTabText(1, "B"); + tabbar.setTabText(2, "C"); + QVERIFY(tabbar.sizeHint().width() <= tabbar.width() && tabbar.sizeHint().height() <= tabbar.height()); + + { + QWheelEvent event(wheelPoint, tabbar.mapToGlobal(wheelPoint), -delta, -delta, + Qt::NoButton, Qt::NoModifier, phase, false, + Qt::MouseEventSynthesizedByApplication, &pixelPad); + QApplication::sendEvent(&tabbar, &event); + QVERIFY(!event.isAccepted()); + } + + { + QWheelEvent event(wheelPoint, tabbar.mapToGlobal(wheelPoint), delta, delta, + Qt::NoButton, Qt::NoModifier, phase, false, + Qt::MouseEventSynthesizedByApplication, &pixelPad); + QApplication::sendEvent(&tabbar, &event); + QVERIFY(!event.isAccepted()); + } +} + #endif // QT_CONFIG(wheelevent) void tst_QTabBar::scrollButtons_data()