From: Claudio Cambra Date: Sat, 29 Oct 2022 17:24:54 +0000 (+0200) Subject: Update Kirigami WheelHandler to upstream v5.99.0 X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~11^2~169^2~1 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=7f9a85a3d041d8ecf42c7966eec6611668edc70a;p=nextcloud-desktop.git Update Kirigami WheelHandler to upstream v5.99.0 Signed-off-by: Claudio Cambra --- diff --git a/src/3rdparty/kirigami/wheelhandler.cpp b/src/3rdparty/kirigami/wheelhandler.cpp index fdfcee044..9dacae4db 100644 --- a/src/3rdparty/kirigami/wheelhandler.cpp +++ b/src/3rdparty/kirigami/wheelhandler.cpp @@ -6,275 +6,621 @@ #include "wheelhandler.h" #include -#include -#include +#include -class GlobalWheelFilterSingleton -{ -public: - GlobalWheelFilter self; -}; - -Q_GLOBAL_STATIC(GlobalWheelFilterSingleton, privateGlobalWheelFilterSelf) - -GlobalWheelFilter::GlobalWheelFilter(QObject *parent) +KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent) : QObject(parent) { } -GlobalWheelFilter::~GlobalWheelFilter() = default; +KirigamiWheelEvent::~KirigamiWheelEvent() +{ +} -GlobalWheelFilter *GlobalWheelFilter::self() +void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event) { - return &privateGlobalWheelFilterSelf()->self; + m_x = event->position().x(); + m_y = event->position().y(); + m_angleDelta = event->angleDelta(); + m_pixelDelta = event->pixelDelta(); + m_buttons = event->buttons(); + m_modifiers = event->modifiers(); + m_accepted = false; + m_inverted = event->inverted(); } -void GlobalWheelFilter::setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler) +qreal KirigamiWheelEvent::x() const { - if (!m_handlersForItem.contains(handler->target())) { - handler->target()->installEventFilter(this); - } - m_handlersForItem.insert(item, handler); + return m_x; +} - connect(item, &QObject::destroyed, this, [this](QObject *obj) { - auto item = dynamic_cast(obj); - m_handlersForItem.remove(item); - }); +qreal KirigamiWheelEvent::y() const +{ + return m_y; +} - connect(handler, &QObject::destroyed, this, [this](QObject *obj) { - auto handler = dynamic_cast(obj); - removeItemHandlerAssociation(handler->target(), handler); - }); +QPointF KirigamiWheelEvent::angleDelta() const +{ + return m_angleDelta; } -void GlobalWheelFilter::removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler) +QPointF KirigamiWheelEvent::pixelDelta() const { - if (!item || !handler) { - return; - } - m_handlersForItem.remove(item, handler); - if (!m_handlersForItem.contains(item)) { - item->removeEventFilter(this); - } + return m_pixelDelta; } -bool GlobalWheelFilter::eventFilter(QObject *watched, QEvent *event) +int KirigamiWheelEvent::buttons() const { - if (event->type() == QEvent::Wheel) { - auto item = qobject_cast(watched); - if (!item || !item->isEnabled()) { - return QObject::eventFilter(watched, event); - } - auto we = dynamic_cast(event); - m_wheelEvent.initializeFromEvent(we); + return m_buttons; +} - bool shouldBlock = false; - bool shouldScrollFlickable = false; +int KirigamiWheelEvent::modifiers() const +{ + return m_modifiers; +} - for (auto *handler : m_handlersForItem.values(item)) { - if (handler->m_blockTargetWheel) { - shouldBlock = true; - } - if (handler->m_scrollFlickableTarget) { - shouldScrollFlickable = true; - } - emit handler->wheel(&m_wheelEvent); - } +bool KirigamiWheelEvent::inverted() const +{ + return m_inverted; +} - if (shouldScrollFlickable && !m_wheelEvent.isAccepted()) { - manageWheel(item, we); - } +bool KirigamiWheelEvent::isAccepted() +{ + return m_accepted; +} - if (shouldBlock) { - return true; - } - } - return QObject::eventFilter(watched, event); +void KirigamiWheelEvent::setAccepted(bool accepted) +{ + m_accepted = accepted; } -void GlobalWheelFilter::manageWheel(QQuickItem *target, QWheelEvent *event) +/////////////////////////////// + +WheelFilterItem::WheelFilterItem(QQuickItem *parent) + : QQuickItem(parent) { - // Duck typing: accept everyhint that has all the properties we need - if (target->metaObject()->indexOfProperty("contentX") == -1 - || target->metaObject()->indexOfProperty("contentY") == -1 - || target->metaObject()->indexOfProperty("contentWidth") == -1 - || target->metaObject()->indexOfProperty("contentHeight") == -1 - || target->metaObject()->indexOfProperty("topMargin") == -1 - || target->metaObject()->indexOfProperty("bottomMargin") == -1 - || target->metaObject()->indexOfProperty("leftMargin") == -1 - || target->metaObject()->indexOfProperty("rightMargin") == -1 - || target->metaObject()->indexOfProperty("originX") == -1 - || target->metaObject()->indexOfProperty("originY") == -1) { - return; - } + setEnabled(false); +} - qreal contentWidth = target->property("contentWidth").toReal(); - qreal contentHeight = target->property("contentHeight").toReal(); - qreal contentX = target->property("contentX").toReal(); - qreal contentY = target->property("contentY").toReal(); - qreal topMargin = target->property("topMargin").toReal(); - qreal bottomMargin = target->property("bottomMargin").toReal(); - qreal leftMargin = target->property("leftMaring").toReal(); - qreal rightMargin = target->property("rightMargin").toReal(); - qreal originX = target->property("originX").toReal(); - qreal originY = target->property("originY").toReal(); +/////////////////////////////// - // Scroll Y - if (contentHeight > target->height()) { +WheelHandler::WheelHandler(QObject *parent) + : QObject(parent) + , m_filterItem(new WheelFilterItem(nullptr)) +{ + m_filterItem->installEventFilter(this); - int y = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8; + m_wheelScrollingTimer.setSingleShot(true); + m_wheelScrollingTimer.setInterval(m_wheelScrollingDuration); + m_wheelScrollingTimer.callOnTimeout([this]() { + setScrolling(false); + }); - //if we don't have a pixeldelta, apply the configured mouse wheel lines - if (!event->pixelDelta().y()) { - y *= 3; // Magic copied value from Kirigami::Settings + connect(QGuiApplication::styleHints(), &QStyleHints::wheelScrollLinesChanged, this, [this](int scrollLines) { + m_defaultPixelStepSize = 20 * scrollLines; + if (!m_explicitVStepSize && m_verticalStepSize != m_defaultPixelStepSize) { + m_verticalStepSize = m_defaultPixelStepSize; + Q_EMIT verticalStepSizeChanged(); } - - // Scroll one page regardless of delta: - if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) { - if (y > 0) { - y = target->height(); - } else if (y < 0) { - y = -target->height(); - } + if (!m_explicitHStepSize && m_horizontalStepSize != m_defaultPixelStepSize) { + m_horizontalStepSize = m_defaultPixelStepSize; + Q_EMIT horizontalStepSizeChanged(); } + }); +} - qreal minYExtent = topMargin - originY; - qreal maxYExtent = target->height() - (contentHeight + bottomMargin + originY); +WheelHandler::~WheelHandler() = default; - target->setProperty("contentY", qMin(-maxYExtent, qMax(-minYExtent, contentY - y))); - } +QQuickItem *WheelHandler::target() const +{ + return m_flickable; +} - //Scroll X - if (contentWidth > target->width()) { +void WheelHandler::setTarget(QQuickItem *target) +{ + if (m_flickable == target) { + return; + } - int x = event->pixelDelta().x() != 0 ? event->pixelDelta().x() : event->angleDelta().x() / 8; + if (target && !target->inherits("QQuickFlickable")) { + qmlWarning(this) << "target must be a QQuickFlickable"; + return; + } - // Special case: when can't scroll vertically, scroll horizontally with vertical wheel as well - if (x == 0 && contentHeight <= target->height()) { - x = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8; - } + if (m_flickable) { + m_flickable->removeEventFilter(this); + disconnect(m_flickable, nullptr, m_filterItem, nullptr); + } - //if we don't have a pixeldelta, apply the configured mouse wheel lines - if (!event->pixelDelta().x()) { - x *= 3; // Magic copied value from Kirigami::Settings + m_flickable = target; + m_filterItem->setParentItem(target); + + QQuickItem *vscrollbar = nullptr; + QQuickItem *hscrollbar = nullptr; + + if (target) { + target->installEventFilter(this); + + // Stack WheelFilterItem over the Flickable's scrollable content + m_filterItem->stackAfter(target->property("contentItem").value()); + // Make it fill the Flickable + m_filterItem->setWidth(target->width()); + m_filterItem->setHeight(target->height()); + connect(target, &QQuickItem::widthChanged, m_filterItem, [this, target](){ + m_filterItem->setWidth(target->width()); + }); + connect(target, &QQuickItem::heightChanged, m_filterItem, [this, target](){ + m_filterItem->setHeight(target->height()); + }); + + // Get ScrollBars so that we can filter them too, even if they're not in the bounds of the Flickable + auto targetChildren = target->children(); + for (auto child : targetChildren) { + if (child->inherits("QQuickScrollBarAttached")) { + vscrollbar = child->property("vertical").value(); + hscrollbar = child->property("horizontal").value(); + break; + } } - - // Scroll one page regardless of delta: - if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) { - if (x > 0) { - x = target->width(); - } else if (x < 0) { - x = -target->width(); + // Check ScrollView if there are no scrollbars attached to the Flickable. + // We need to check if the parent inherits QQuickScrollView in case the + // parent is another Flickable that already has a Kirigami WheelHandler. + auto targetParent = target->parentItem(); + if (targetParent && targetParent->inherits("QQuickScrollView") && !vscrollbar && !hscrollbar) { + auto targetParentChildren = targetParent->children(); + for (auto child : targetParentChildren) { + if (child->inherits("QQuickScrollBarAttached")) { + vscrollbar = child->property("vertical").value(); + hscrollbar = child->property("horizontal").value(); + break; + } } } + } - qreal minXExtent = leftMargin - originX; - qreal maxXExtent = target->width() - (contentWidth + rightMargin + originX); + if (m_verticalScrollBar != vscrollbar) { + if (m_verticalScrollBar) { + m_verticalScrollBar->removeEventFilter(this); + } + m_verticalScrollBar = vscrollbar; + if (vscrollbar) { + vscrollbar->installEventFilter(this); + } + } - target->setProperty("contentX", qMin(-maxXExtent, qMax(-minXExtent, contentX - x))); + if (m_horizontalScrollBar != hscrollbar) { + if (m_horizontalScrollBar) { + m_horizontalScrollBar->removeEventFilter(this); + } + m_horizontalScrollBar = hscrollbar; + if (hscrollbar) { + hscrollbar->installEventFilter(this); + } } - //this is just for making the scrollbar - target->metaObject()->invokeMethod(target, "flick", Q_ARG(double, 0), Q_ARG(double, 1)); - target->metaObject()->invokeMethod(target, "cancelFlick"); + Q_EMIT targetChanged(); } +qreal WheelHandler::verticalStepSize() const +{ + return m_verticalStepSize; +} -//////////////////////////// -KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent) - : QObject(parent) -{} +void WheelHandler::setVerticalStepSize(qreal stepSize) +{ + m_explicitVStepSize = true; + if (qFuzzyCompare(m_verticalStepSize, stepSize)) { + return; + } + // Mimic the behavior of QQuickScrollBar when stepSize is 0 + if (qFuzzyIsNull(stepSize)) { + resetVerticalStepSize(); + return; + } + m_verticalStepSize = stepSize; + Q_EMIT verticalStepSizeChanged(); +} -KirigamiWheelEvent::~KirigamiWheelEvent() = default; +void WheelHandler::resetVerticalStepSize() +{ + m_explicitVStepSize = false; + if (qFuzzyCompare(m_verticalStepSize, m_defaultPixelStepSize)) { + return; + } + m_verticalStepSize = m_defaultPixelStepSize; + Q_EMIT verticalStepSizeChanged(); +} -void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event) +qreal WheelHandler::horizontalStepSize() const { - m_x = event->position().x(); - m_y = event->position().y(); - m_angleDelta = event->angleDelta(); - m_pixelDelta = event->pixelDelta(); - m_buttons = event->buttons(); - m_modifiers = event->modifiers(); - m_accepted = false; - m_inverted = event->inverted(); + return m_horizontalStepSize; } -qreal KirigamiWheelEvent::x() const +void WheelHandler::setHorizontalStepSize(qreal stepSize) { - return m_x; + m_explicitHStepSize = true; + if (qFuzzyCompare(m_horizontalStepSize, stepSize)) { + return; + } + // Mimic the behavior of QQuickScrollBar when stepSize is 0 + if (qFuzzyIsNull(stepSize)) { + resetHorizontalStepSize(); + return; + } + m_horizontalStepSize = stepSize; + Q_EMIT horizontalStepSizeChanged(); } -qreal KirigamiWheelEvent::y() const +void WheelHandler::resetHorizontalStepSize() { - return m_y; + m_explicitHStepSize = false; + if (qFuzzyCompare(m_horizontalStepSize, m_defaultPixelStepSize)) { + return; + } + m_horizontalStepSize = m_defaultPixelStepSize; + Q_EMIT horizontalStepSizeChanged(); } -QPointF KirigamiWheelEvent::angleDelta() const +Qt::KeyboardModifiers WheelHandler::pageScrollModifiers() const { - return m_angleDelta; + return m_pageScrollModifiers; } -QPointF KirigamiWheelEvent::pixelDelta() const +void WheelHandler::setPageScrollModifiers(Qt::KeyboardModifiers modifiers) { - return m_pixelDelta; + if (m_pageScrollModifiers == modifiers) { + return; + } + m_pageScrollModifiers = modifiers; + Q_EMIT pageScrollModifiersChanged(); } -int KirigamiWheelEvent::buttons() const +void WheelHandler::resetPageScrollModifiers() { - return m_buttons; + setPageScrollModifiers(m_defaultPageScrollModifiers); } -int KirigamiWheelEvent::modifiers() const +bool WheelHandler::filterMouseEvents() const { - return m_modifiers; + return m_filterMouseEvents; } -bool KirigamiWheelEvent::inverted() const +void WheelHandler::setFilterMouseEvents(bool enabled) { - return m_inverted; + if (m_filterMouseEvents == enabled) { + return; + } + m_filterMouseEvents = enabled; + Q_EMIT filterMouseEventsChanged(); } -bool KirigamiWheelEvent::isAccepted() +bool WheelHandler::keyNavigationEnabled() const { - return m_accepted; + return m_keyNavigationEnabled; } -void KirigamiWheelEvent::setAccepted(bool accepted) +void WheelHandler::setKeyNavigationEnabled(bool enabled) { - m_accepted = accepted; + if (m_keyNavigationEnabled == enabled) { + return; + } + m_keyNavigationEnabled = enabled; + Q_EMIT keyNavigationEnabledChanged(); } +void WheelHandler::setScrolling(bool scrolling) +{ + if (m_wheelScrolling == scrolling) { + if (m_wheelScrolling) { + m_wheelScrollingTimer.start(); + } + return; + } + m_wheelScrolling = scrolling; + m_filterItem->setEnabled(m_wheelScrolling); +} -/////////////////////////////// +bool WheelHandler::scrollFlickable(QPointF pixelDelta, QPointF angleDelta, Qt::KeyboardModifiers modifiers) +{ + if (!m_flickable || (pixelDelta.isNull() && angleDelta.isNull())) { + return false; + } -WheelHandler::WheelHandler(QObject *parent) - : QObject(parent) + const qreal width = m_flickable->width(); + const qreal height = m_flickable->height(); + const qreal contentWidth = m_flickable->property("contentWidth").toReal(); + const qreal contentHeight = m_flickable->property("contentHeight").toReal(); + const qreal contentX = m_flickable->property("contentX").toReal(); + const qreal contentY = m_flickable->property("contentY").toReal(); + const qreal topMargin = m_flickable->property("topMargin").toReal(); + const qreal bottomMargin = m_flickable->property("bottomMargin").toReal(); + const qreal leftMargin = m_flickable->property("leftMargin").toReal(); + const qreal rightMargin = m_flickable->property("rightMargin").toReal(); + const qreal originX = m_flickable->property("originX").toReal(); + const qreal originY = m_flickable->property("originY").toReal(); + const qreal pageWidth = width - leftMargin - rightMargin; + const qreal pageHeight = height - topMargin - bottomMargin; + const auto window = m_flickable->window(); + const qreal devicePixelRatio = window != nullptr ? window->devicePixelRatio() : qGuiApp->devicePixelRatio(); + + // HACK: Only transpose deltas when not using xcb in order to not conflict with xcb's own delta transposing + if (modifiers & m_defaultHorizontalScrollModifiers && qGuiApp->platformName() != QLatin1String("xcb")) { + angleDelta = angleDelta.transposed(); + pixelDelta = pixelDelta.transposed(); + } + + const qreal xTicks = angleDelta.x() / 120; + const qreal yTicks = angleDelta.y() / 120; + qreal xChange; + qreal yChange; + bool scrolled = false; + + // Scroll X + if (contentWidth > pageWidth) { + // Use page size with pageScrollModifiers. Matches QScrollBar, which uses QAbstractSlider behavior. + if (modifiers & m_pageScrollModifiers) { + xChange = qBound(-pageWidth, xTicks * pageWidth, pageWidth); + } else if (pixelDelta.x() != 0) { + xChange = pixelDelta.x(); + } else { + xChange = xTicks * m_horizontalStepSize; + } + + // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs + + qreal minXExtent = leftMargin - originX; + qreal maxXExtent = width - (contentWidth + rightMargin + originX); + + qreal newContentX = qBound(-minXExtent, contentX - xChange, -maxXExtent); + // Flickable::pixelAligned rounds the position, so round to mimic that behavior. + // Rounding prevents fractional positioning from causing text to be + // clipped off on the top and bottom. + // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio + // after to make position match pixels on the screen more closely. + newContentX = std::round(newContentX * devicePixelRatio) / devicePixelRatio; + if (contentX != newContentX) { + scrolled = true; + m_flickable->setProperty("contentX", newContentX); + } + } + + // Scroll Y + if (contentHeight > pageHeight) { + if (modifiers & m_pageScrollModifiers) { + yChange = qBound(-pageHeight, yTicks * pageHeight, pageHeight); + } else if (pixelDelta.y() != 0) { + yChange = pixelDelta.y(); + } else { + yChange = yTicks * m_verticalStepSize; + } + + // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs + + qreal minYExtent = topMargin - originY; + qreal maxYExtent = height - (contentHeight + bottomMargin + originY); + + qreal newContentY = qBound(-minYExtent, contentY - yChange, -maxYExtent); + // Flickable::pixelAligned rounds the position, so round to mimic that behavior. + // Rounding prevents fractional positioning from causing text to be + // clipped off on the top and bottom. + // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio + // after to make position match pixels on the screen more closely. + newContentY = std::round(newContentY * devicePixelRatio) / devicePixelRatio; + if (contentY != newContentY) { + scrolled = true; + m_flickable->setProperty("contentY", newContentY); + } + } + + return scrolled; +} + +bool WheelHandler::scrollUp(qreal stepSize) { + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_verticalStepSize; + } + // contentY uses reversed sign + return scrollFlickable(QPointF(0, stepSize)); } -WheelHandler::~WheelHandler() = default; +bool WheelHandler::scrollDown(qreal stepSize) +{ + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_verticalStepSize; + } + // contentY uses reversed sign + return scrollFlickable(QPointF(0, -stepSize)); +} -QQuickItem *WheelHandler::target() const +bool WheelHandler::scrollLeft(qreal stepSize) { - return m_target; + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_horizontalStepSize; + } + // contentX uses reversed sign + return scrollFlickable(QPoint(stepSize, 0)); } -void WheelHandler::setTarget(QQuickItem *target) +bool WheelHandler::scrollRight(qreal stepSize) { - if (m_target == target) { - return; + if (qFuzzyIsNull(stepSize)) { + return false; + } else if (stepSize < 0) { + stepSize = m_horizontalStepSize; } + // contentX uses reversed sign + return scrollFlickable(QPoint(-stepSize, 0)); +} - if (m_target) { - GlobalWheelFilter::self()->removeItemHandlerAssociation(m_target, this); +bool WheelHandler::eventFilter(QObject *watched, QEvent *event) +{ + auto item = qobject_cast(watched); + if (!item || !item->isEnabled()) { + return false; } - m_target = target; + qreal contentWidth = 0; + qreal contentHeight = 0; + qreal pageWidth = 0; + qreal pageHeight = 0; + if (m_flickable) { + contentWidth = m_flickable->property("contentWidth").toReal(); + contentHeight = m_flickable->property("contentHeight").toReal(); + pageWidth = m_flickable->width() - m_flickable->property("leftMargin").toReal() - m_flickable->property("rightMargin").toReal(); + pageHeight = m_flickable->height() - m_flickable->property("topMargin").toReal() - m_flickable->property("bottomMargin").toReal(); + } - GlobalWheelFilter::self()->setItemHandlerAssociation(target, this); + // The code handling touch, mouse and hover events is mostly copied/adapted from QQuickScrollView::childMouseEventFilter() + switch (event->type()) { + case QEvent::Wheel: { + // QQuickScrollBar::interactive handling Matches behavior in QQuickScrollView::eventFilter() + if (m_filterMouseEvents) { + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", true); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", true); + } + } + QWheelEvent *wheelEvent = static_cast(event); + + // NOTE: On X11 with libinput, pixelDelta is identical to angleDelta when using a mouse that shouldn't use pixelDelta. + // If faulty pixelDelta, reset pixelDelta to (0,0). + if (wheelEvent->pixelDelta() == wheelEvent->angleDelta()) { + // In order to change any of the data, we have to create a whole new QWheelEvent from its constructor. + QWheelEvent newWheelEvent( + wheelEvent->position(), + wheelEvent->globalPosition(), + QPoint(0,0), // pixelDelta + wheelEvent->angleDelta(), + wheelEvent->buttons(), + wheelEvent->modifiers(), + wheelEvent->phase(), + wheelEvent->inverted(), + wheelEvent->source() + ); + m_kirigamiWheelEvent.initializeFromEvent(&newWheelEvent); + } else { + m_kirigamiWheelEvent.initializeFromEvent(wheelEvent); + } - emit targetChanged(); -} + Q_EMIT wheel(&m_kirigamiWheelEvent); + + if (m_kirigamiWheelEvent.isAccepted()) { + return true; + } + + bool scrolled = false; + if (m_scrollFlickableTarget || (contentHeight <= pageHeight && contentWidth <= pageWidth)) { + // Don't use pixelDelta from the event unless angleDelta is not available + // because scrolling by pixelDelta is too slow on Wayland with libinput. + QPointF pixelDelta = m_kirigamiWheelEvent.angleDelta().isNull() ? m_kirigamiWheelEvent.pixelDelta() : QPoint(0, 0); + scrolled = scrollFlickable(pixelDelta, + m_kirigamiWheelEvent.angleDelta(), + Qt::KeyboardModifiers(m_kirigamiWheelEvent.modifiers())); + } + setScrolling(scrolled); + + // NOTE: Wheel events created by touchpad gestures with pixel deltas will cause scrolling to jump back + // to where scrolling started unless the event is always accepted before it reaches the Flickable. + bool flickableWillUseGestureScrolling = !(wheelEvent->source() == Qt::MouseEventNotSynthesized || wheelEvent->pixelDelta().isNull()); + return scrolled || m_blockTargetWheel || flickableWillUseGestureScrolling; + } + + case QEvent::TouchBegin: { + m_wasTouched = true; + if (!m_filterMouseEvents) { + break; + } + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", false); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", false); + } + break; + } + case QEvent::TouchEnd: { + m_wasTouched = false; + break; + } -#include "moc_wheelhandler.cpp" + case QEvent::MouseButtonPress: { + // NOTE: Flickable does not handle touch events, only synthesized mouse events + m_wasTouched = static_cast(event)->source() != Qt::MouseEventNotSynthesized; + if (!m_filterMouseEvents) { + break; + } + if (!m_wasTouched) { + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", true); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", true); + } + break; + } + return !m_wasTouched && item == m_flickable; + } + + case QEvent::MouseMove: + case QEvent::MouseButtonRelease: { + setScrolling(false); + if (!m_filterMouseEvents) { + break; + } + if (static_cast(event)->source() == Qt::MouseEventNotSynthesized && item == m_flickable) { + return true; + } + break; + } + + case QEvent::HoverEnter: + case QEvent::HoverMove: { + if (!m_filterMouseEvents) { + break; + } + if (m_wasTouched && (item == m_verticalScrollBar || item == m_horizontalScrollBar)) { + if (m_verticalScrollBar) { + m_verticalScrollBar->setProperty("interactive", true); + } + if (m_horizontalScrollBar) { + m_horizontalScrollBar->setProperty("interactive", true); + } + } + break; + } + + case QEvent::KeyPress: { + if (!m_keyNavigationEnabled) { + break; + } + QKeyEvent *keyEvent = static_cast(event); + bool horizontalScroll = keyEvent->modifiers() & m_defaultHorizontalScrollModifiers; + switch (keyEvent->key()) { + case Qt::Key_Up: return scrollUp(); + case Qt::Key_Down: return scrollDown(); + case Qt::Key_Left: return scrollLeft(); + case Qt::Key_Right: return scrollRight(); + case Qt::Key_PageUp: return horizontalScroll ? scrollLeft(pageWidth) : scrollUp(pageHeight); + case Qt::Key_PageDown: return horizontalScroll ? scrollRight(pageWidth) : scrollDown(pageHeight); + case Qt::Key_Home: return horizontalScroll ? scrollLeft(contentWidth) : scrollUp(contentHeight); + case Qt::Key_End: return horizontalScroll ? scrollRight(contentWidth) : scrollDown(contentHeight); + default: break; + } + break; + } + + default: break; + } + + return false; +} diff --git a/src/3rdparty/kirigami/wheelhandler.h b/src/3rdparty/kirigami/wheelhandler.h index 41f6fb954..d921a6c9d 100644 --- a/src/3rdparty/kirigami/wheelhandler.h +++ b/src/3rdparty/kirigami/wheelhandler.h @@ -1,18 +1,18 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * SPDX-License-Identifier: LGPL-2.0-or-later +/* SPDX-FileCopyrightText: 2019 Marco Martin + * SPDX-FileCopyrightText: 2021 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once -#include +#include +#include #include #include -#include +#include +#include class QWheelEvent; - class WheelHandler; /** @@ -111,13 +111,13 @@ public: void initializeFromEvent(QWheelEvent *event); - [[nodiscard]] qreal x() const; - [[nodiscard]] qreal y() const; - [[nodiscard]] QPointF angleDelta() const; - [[nodiscard]] QPointF pixelDelta() const; - [[nodiscard]] int buttons() const; - [[nodiscard]] int modifiers() const; - [[nodiscard]] bool inverted() const; + qreal x() const; + qreal y() const; + QPointF angleDelta() const; + QPointF pixelDelta() const; + int buttons() const; + int modifiers() const; + bool inverted() const; bool isAccepted(); void setAccepted(bool accepted); @@ -132,59 +132,135 @@ private: bool m_accepted = false; }; -class GlobalWheelFilter : public QObject +class WheelFilterItem : public QQuickItem { Q_OBJECT - public: - GlobalWheelFilter(QObject *parent = nullptr); - ~GlobalWheelFilter() override; - - static GlobalWheelFilter *self(); - - void setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler); - void removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler); - -protected: - bool eventFilter(QObject *watched, QEvent *event) override; - -private: - void manageWheel(QQuickItem *target, QWheelEvent *wheel); - - QMultiHash m_handlersForItem; - KirigamiWheelEvent m_wheelEvent; + WheelFilterItem(QQuickItem *parent = nullptr); }; - - /** - * This class intercepts the mouse wheel events of its target, and gives them to the user code as a signal, which can be used for custom mouse wheel management code. - * The handler can block completely the wheel events from its target, and if it's a Flickable, it can automatically handle scrolling on it + * @brief Handles scrolling for a Flickable and 2 attached ScrollBars. + * + * WheelHandler filters events from a Flickable, a vertical ScrollBar and a horizontal ScrollBar. + * Wheel and KeyPress events (when `keyNavigationEnabled` is true) are used to scroll the Flickable. + * When `filterMouseEvents` is true, WheelHandler blocks mouse button input from reaching the Flickable + * and sets the `interactive` property of the scrollbars to false when touch input is used. + * + * Wheel event handling behavior: + * + * - Pixel delta is ignored unless angle delta is not available because pixel delta scrolling is too slow. Qt Widgets doesn't use pixel delta either, so the default scroll speed should be consistent with Qt Widgets. + * - When using angle delta, scroll using the step increments defined by `verticalStepSize` and `horizontalStepSize`. + * - When one of the keyboard modifiers in `pageScrollModifiers` is used, scroll by pages. + * - When using a device that doesn't use 120 angle delta unit increments such as a touchpad, the `verticalStepSize`, `horizontalStepSize` and page increments (if using page scrolling) will be multiplied by `angle delta / 120` to keep scrolling smooth. + * - If scrolling has happened in the last 400ms, use an internal QQuickItem stacked over the Flickable's contentItem to catch wheel events and use those wheel events to scroll, if possible. This prevents controls inside the Flickable's contentItem that allow scrolling to change the value (e.g., Sliders, SpinBoxes) from conflicting with scrolling the page. + * + * Common usage with a Flickable: + * + * @include wheelhandler/FlickableUsage.qml + * + * Common usage inside of a ScrollView template: + * + * @include wheelhandler/ScrollViewUsage.qml + * */ class WheelHandler : public QObject { Q_OBJECT /** - * target: Item + * @brief This property holds the Qt Quick Flickable that the WheelHandler will control. + */ + Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged FINAL) + + /** + * @brief This property holds the vertical step size. + * + * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea. + * + * @sa horizontalStepSize * - * The target we want to manage wheel events. - * We will receive wheel() signals every time the user moves - * the mouse wheel (or scrolls with the touchpad) on top - * of that item. + * @since KDE Frameworks 5.89 */ - Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged) + Q_PROPERTY(qreal verticalStepSize READ verticalStepSize + WRITE setVerticalStepSize RESET resetVerticalStepSize + NOTIFY verticalStepSizeChanged FINAL) /** - * blockTargetWheel: bool + * @brief This property holds the horizontal step size. * - * If true, the target won't receive any wheel event at all (default true) + * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea. + * + * @sa verticalStepSize + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(qreal horizontalStepSize READ horizontalStepSize + WRITE setHorizontalStepSize RESET resetHorizontalStepSize + NOTIFY horizontalStepSizeChanged FINAL) + + /** + * @brief This property holds the keyboard modifiers that will be used to start page scrolling. + * + * The default value is equivalent to `Qt.ControlModifier | Qt.ShiftModifier`. This matches QScrollBar, which uses QAbstractSlider behavior. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers + WRITE setPageScrollModifiers RESET resetPageScrollModifiers + NOTIFY pageScrollModifiersChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler filters mouse events like a Qt Quick Controls ScrollView would. + * + * Touch events are allowed to flick the view and they make the scrollbars not interactive. + * + * Mouse events are not allowed to flick the view and they make the scrollbars interactive. + * + * Hover events on the scrollbars and wheel events on anything also make the scrollbars interactive when this property is set to true. + * + * The default value is `false`. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(bool filterMouseEvents READ filterMouseEvents + WRITE setFilterMouseEvents NOTIFY filterMouseEventsChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler handles keyboard scrolling. + * + * - Left arrow scrolls a step to the left. + * - Right arrow scrolls a step to the right. + * - Up arrow scrolls a step upwards. + * - Down arrow scrolls a step downwards. + * - PageUp scrolls to the previous page. + * - PageDown scrolls to the next page. + * - Home scrolls to the beginning. + * - End scrolls to the end. + * - When Alt is held, scroll horizontally when using PageUp, PageDown, Home or End. + * + * The default value is `false`. + * + * @since KDE Frameworks 5.89 + */ + Q_PROPERTY(bool keyNavigationEnabled READ keyNavigationEnabled + WRITE setKeyNavigationEnabled NOTIFY keyNavigationEnabledChanged FINAL) + + /** + * @brief This property holds whether the WheelHandler blocks all wheel events from reaching the Flickable. + * + * When this property is false, scrolling the Flickable with WheelHandler will only block an event from reaching the Flickable if the Flickable is actually scrolled by WheelHandler. + * + * NOTE: Wheel events created by touchpad gestures with pixel deltas will always be accepted no matter what. This is because they will cause the Flickable to jump back to where scrolling started unless the events are always accepted before they reach the Flickable. + * + * The default value is true. */ Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged) /** - * scrollFlickableTarget: bool - * If this property is true and the target is a Flickable, wheel events will cause the Flickable to scroll (default true) + * @brief This property holds whether the WheelHandler can use wheel events to scroll the Flickable. + * + * The default value is true. */ Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged) @@ -192,22 +268,111 @@ public: explicit WheelHandler(QObject *parent = nullptr); ~WheelHandler() override; - [[nodiscard]] QQuickItem *target() const; + QQuickItem *target() const; void setTarget(QQuickItem *target); + qreal verticalStepSize() const; + void setVerticalStepSize(qreal stepSize); + void resetVerticalStepSize(); + + qreal horizontalStepSize() const; + void setHorizontalStepSize(qreal stepSize); + void resetHorizontalStepSize(); + + Qt::KeyboardModifiers pageScrollModifiers() const; + void setPageScrollModifiers(Qt::KeyboardModifiers modifiers); + void resetPageScrollModifiers(); + + bool filterMouseEvents() const; + void setFilterMouseEvents(bool enabled); + + bool keyNavigationEnabled() const; + void setKeyNavigationEnabled(bool enabled); + + /** + * Scroll up one step. If the stepSize parameter is less than 0, the verticalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollUp(qreal stepSize = -1); + + /** + * Scroll down one step. If the stepSize parameter is less than 0, the verticalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollDown(qreal stepSize = -1); + + /** + * Scroll left one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollLeft(qreal stepSize = -1); + + /** + * Scroll right one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used. + * + * returns true if the contentItem was moved. + * + * @since KDE Frameworks 5.89 + */ + Q_INVOKABLE bool scrollRight(qreal stepSize = -1); + Q_SIGNALS: void targetChanged(); + void verticalStepSizeChanged(); + void horizontalStepSizeChanged(); + void pageScrollModifiersChanged(); + void filterMouseEventsChanged(); + void keyNavigationEnabledChanged(); void blockTargetWheelChanged(); void scrollFlickableTargetChanged(); + + /** + * @brief This signal is emitted when a wheel event reaches the event filter, just before scrolling is handled. + * + * Accepting the wheel event in the `onWheel` signal handler prevents scrolling from happening. + */ void wheel(KirigamiWheelEvent *wheel); +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + private: - QPointer m_target; + void setScrolling(bool scrolling); + bool scrollFlickable(QPointF pixelDelta, + QPointF angleDelta = {}, + Qt::KeyboardModifiers modifiers = Qt::NoModifier); + + QPointer m_flickable; + QPointer m_verticalScrollBar; + QPointer m_horizontalScrollBar; + QPointer m_filterItem; + // Matches QScrollArea and QTextEdit + qreal m_defaultPixelStepSize = 20 * QGuiApplication::styleHints()->wheelScrollLines(); + qreal m_verticalStepSize = m_defaultPixelStepSize; + qreal m_horizontalStepSize = m_defaultPixelStepSize; + bool m_explicitVStepSize = false; + bool m_explicitHStepSize = false; + bool m_wheelScrolling = false; + constexpr static qreal m_wheelScrollingDuration = 400; + bool m_filterMouseEvents = false; + bool m_keyNavigationEnabled = false; + bool m_wasTouched = false; bool m_blockTargetWheel = true; bool m_scrollFlickableTarget = true; - KirigamiWheelEvent m_wheelEvent; - - friend class GlobalWheelFilter; + // Same as QXcbWindow. + constexpr static Qt::KeyboardModifiers m_defaultHorizontalScrollModifiers = Qt::AltModifier; + // Same as QScrollBar/QAbstractSlider. + constexpr static Qt::KeyboardModifiers m_defaultPageScrollModifiers = Qt::ControlModifier | Qt::ShiftModifier; + Qt::KeyboardModifiers m_pageScrollModifiers = m_defaultPageScrollModifiers; + QTimer m_wheelScrollingTimer; + KirigamiWheelEvent m_kirigamiWheelEvent; }; - -