diff options
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 9 | ||||
| -rw-r--r-- | src/threadlistview.cpp | 33 | ||||
| -rw-r--r-- | src/threadlistview.h | 13 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 48 |
5 files changed, 103 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ad3d8..21744de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ point at which they are stable. ### Changed - The status bar renders its messages as plain text. +- The thread list scrolls smoothly. A mouse wheel notch glides one card + instead of jumping several, and a touchpad moves the list continuously. ### Fixed diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 19373e3..e93898c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -962,8 +962,13 @@ void MainWindow::buildUi() // the viewport and auto-scroll brought the clicked index fully into view. m_threadView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - // Scrolling a whole card at a time rather than a fraction of one, so a - // card is never left half above the top edge. + // Per PIXEL, so a touchpad moves the list continuously. Qt's default is + // per ITEM, which jumped several whole cards per wheel notch and quantised + // a touchpad's small deltas into card-sized jumps, so the eye lost track + // of how far the list had moved. The cost is that a card can rest half + // above the top edge. A wheel notch glides one step, which is one card; + // see ThreadListView::wheelEvent. + m_threadView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_threadView->verticalScrollBar()->setSingleStep( CardLayout::heightFor(m_threadView->font())); diff --git a/src/threadlistview.cpp b/src/threadlistview.cpp index 019d7c9..b5176bf 100644 --- a/src/threadlistview.cpp +++ b/src/threadlistview.cpp @@ -22,6 +22,9 @@ #include "threadlistmodel.h" #include <QMouseEvent> +#include <QPropertyAnimation> +#include <QScrollBar> +#include <QWheelEvent> void ThreadListView::mousePressEvent(QMouseEvent *event) { @@ -53,3 +56,33 @@ void ThreadListView::mousePressEvent(QMouseEvent *event) QTreeView::mousePressEvent(event); } + +void ThreadListView::wheelEvent(QWheelEvent *event) +{ + const int notches = event->angleDelta().y() / QWheelEvent::DefaultDeltasPerStep; + if (!event->pixelDelta().isNull() || event->modifiers() != Qt::NoModifier + || notches == 0) { + // ponytail: a high-resolution wheel sending less than one notch per + // event falls through to the plain per-pixel scroll, unanimated. + QTreeView::wheelEvent(event); + return; + } + + QScrollBar *bar = verticalScrollBar(); + if (!m_glide) { + m_glide = new QPropertyAnimation(bar, "value", this); + m_glide->setDuration(160); + m_glide->setEasingCurve(QEasingCurve::OutCubic); + } + + const bool gliding = m_glide->state() == QAbstractAnimation::Running; + const int from = gliding ? m_glide->endValue().toInt() : bar->value(); + const int to = qBound(bar->minimum(), from - notches * bar->singleStep(), + bar->maximum()); + + m_glide->stop(); + m_glide->setStartValue(bar->value()); + m_glide->setEndValue(to); + m_glide->start(); + event->accept(); +} diff --git a/src/threadlistview.h b/src/threadlistview.h index a509389..79db424 100644 --- a/src/threadlistview.h +++ b/src/threadlistview.h @@ -20,6 +20,8 @@ #include <QTreeView> +class QPropertyAnimation; + /// The thread list. /// /// It exists for ONE reason now: the expander is drawn by CardDelegate, and a @@ -47,4 +49,15 @@ protected: /// indicator underneath, also removed the style's hit area, so an expander /// once painted correctly and did nothing at all. void mousePressEvent(QMouseEvent *event) override; + + /// A mouse wheel notch GLIDES one scrollbar step instead of jumping, so + /// the eye can follow how far the list moved. Notches arriving mid-glide + /// add to its target rather than restarting from where it has got to. A + /// touchpad's pixel deltas are already continuous and pass straight + /// through, as does any wheel with a modifier held. + void wheelEvent(QWheelEvent *event) override; + +private: + /// Created on the first notch, owned by the view. + QPropertyAnimation *m_glide = nullptr; }; diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 7ef89e7..2b7008d 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -409,6 +409,7 @@ private slots: void theStatusBarFollowsTheSyncPhase(); void aSelectedReadThreadIsNotDimmedIntoTheHighlight(); void childRowsAreIndentedUnderTheirThread(); + void oneWheelNotchGlidesOneCard(); void aThreadWithRepliesDrawsAVisibleExpander(); void cardsNeverScrollSideways(); void selectingAConversationArmsNoMarkRead(); @@ -1157,6 +1158,53 @@ static ThreadSummary makeThread(const QString &id, const QStringList &tags) return thread; } +void TestMainWindow::oneWheelNotchGlidesOneCard() +{ + // The list scrolled per ITEM, Qt's default, so a notch jumped several + // whole cards and a touchpad's small deltas were quantised into card-sized + // jumps: the user lost track of how far the list had moved. Per PIXEL, + // one notch moves exactly one card. + const Config config; + MainWindow window(config); + window.resize(800, 600); + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + + auto *model = window.findChild<ThreadListModel *>(); + QVERIFY(model); + auto *view = window.findChild<ThreadListView *>(); + QVERIFY(view); + + QVector<ThreadSummary> threads; + for (int i = 0; i < 50; ++i) + threads.append(makeThread(QStringLiteral("t%1").arg(i), {})); + model->appendBatch(threads); + + QCOMPARE(view->verticalScrollMode(), QAbstractItemView::ScrollPerPixel); + + QScrollBar *bar = view->verticalScrollBar(); + // The range is laid out lazily after the rows arrive. + QTRY_VERIFY2(bar->maximum() > 0, + "nothing to scroll, so the test measures nothing"); + QCOMPARE(bar->value(), 0); + + const QPoint centre = view->viewport()->rect().center(); + QWheelEvent notch(centre, view->viewport()->mapToGlobal(centre), QPoint(), + QPoint(0, -QWheelEvent::DefaultDeltasPerStep), + Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false); + QApplication::sendEvent(view->viewport(), ¬ch); + + // A glide, not a jump: the first notch is still on its way. + const int card = CardLayout::heightFor(view->font()); + QVERIFY2(bar->value() < card, "the notch jumped rather than glided"); + QTRY_COMPARE(bar->value(), card); + + // A second notch sent mid-glide lands on the NEXT card, not short of it. + QApplication::sendEvent(view->viewport(), ¬ch); + QApplication::sendEvent(view->viewport(), ¬ch); + QTRY_COMPARE(bar->value(), 3 * card); +} + void TestMainWindow::childRowsAreIndentedUnderTheirThread() { const Config config; |
