diff --git a/src/controls/templates/private/ScrollView.qml b/src/controls/templates/private/ScrollView.qml index d28c2b44..d1d44619 100644 --- a/src/controls/templates/private/ScrollView.qml +++ b/src/controls/templates/private/ScrollView.qml @@ -1,143 +1,141 @@ /* * Copyright 2016 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Controls 2.0 import org.kde.kirigami 2.9 as Kirigami -MouseArea { +Item { id: root default property Item contentItem property Flickable flickableItem clip: true //TODO: horizontalScrollBarPolicy is completely noop just for compatibility right now property int horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff property int verticalScrollBarPolicy: Qt.ScrollBarAsNeeded readonly property Item verticalScrollBar: flickableItem.ScrollBar.vertical ? flickableItem.ScrollBar.vertical : null onVerticalScrollBarPolicyChanged: { if (flickableItem.ScrollBar.vertical) { flickableItem.ScrollBar.vertical.visible = verticalScrollBarPolicy != Qt.ScrollBarAlwaysOff; } scrollBarCreationTimer.restart(); } onHorizontalScrollBarPolicyChanged: { if (flickableItem.ScrollBar.horizontal) { flickableItem.ScrollBar.horizontal.visible = horizontalScrollBarPolicy != Qt.ScrollBarAlwaysOff; } scrollBarCreationTimer.restart(); } onContentItemChanged: { if (contentItem.hasOwnProperty("contentY")) { flickableItem = contentItem; if (typeof(flickableItem.keyNavigationEnabled) != "undefined") { flickableItem.keyNavigationEnabled = true; flickableItem.keyNavigationWraps = true; } contentItem.parent = flickableParent; } else { flickableItem = flickableComponent.createObject(flickableParent); contentItem.parent = flickableItem.contentItem; } + flickableItem.Kirigami.WheelHandler.enabled = true; flickableItem.interactive = true; flickableItem.anchors.fill = flickableParent; scrollBarCreationTimer.restart(); } Timer { id: scrollBarCreationTimer interval: 0 onTriggered: { //create or destroy the vertical scrollbar if ((!flickableItem.ScrollBar.vertical) && verticalScrollBarPolicy != Qt.ScrollBarAlwaysOff) { flickableItem.ScrollBar.vertical = verticalScrollComponent.createObject(root); } else if (flickableItem.ScrollBar.vertical && verticalScrollBarPolicy == Qt.ScrollBarAlwaysOff) { flickableItem.ScrollBar.vertical.destroy(); } //create or destroy the horizontal scrollbar if ((!flickableItem.ScrollBar.horizontal) && horizontalScrollBarPolicy != Qt.ScrollBarAlwaysOff) { flickableItem.ScrollBar.horizontal = horizontalScrollComponent.createObject(root); } else if (flickableItem.ScrollBar.horizontal && horizontalScrollBarPolicy == Qt.ScrollBarAlwaysOff) { flickableItem.ScrollBar.horizontal.destroy(); } } } - Kirigami.WheelHandler { - id: wheelHandler - target: root.flickableItem - } + Item { id: flickableParent anchors { fill: parent } } Component { id: flickableComponent Flickable { anchors { fill: parent } contentWidth: root.contentItem ? root.contentItem.width : 0 contentHeight: root.contentItem ? root.contentItem.height : 0 } } Component { id: verticalScrollComponent ScrollBar { z: flickableParent.z + 1 visible: root.contentItem.visible && size < 1 interactive: !Kirigami.Settings.tabletMode //NOTE: use this instead of anchors as crashes on some Qt 5.8 checkouts height: parent.height - anchors.topMargin anchors { topMargin: parent.flickableItem.headerItem ? parent.flickableItem.headerItem.height : 0 right: parent.right top: parent.top } } } Component { id: horizontalScrollComponent ScrollBar { z: flickableParent.z + 1 visible: root.contentItem.visible && size < 1 interactive: !Kirigami.Settings.tabletMode //NOTE: use this instead of anchors as crashes on some Qt 5.8 checkouts height: parent.height - anchors.topMargin anchors { left: parent.left right: parent.right bottom: parent.bottom } } } } diff --git a/src/wheelhandler.cpp b/src/wheelhandler.cpp index 5fc93342..5eefc8e5 100644 --- a/src/wheelhandler.cpp +++ b/src/wheelhandler.cpp @@ -1,244 +1,274 @@ /* * Copyright (C) 2019 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "wheelhandler.h" #include "settings.h" #include #include #include KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent) : QObject(parent) {} KirigamiWheelEvent::~KirigamiWheelEvent() {} void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event) { m_x = event->x(); m_y = event->y(); m_angleDelta = event->angleDelta(); m_pixelDelta = event->pixelDelta(); m_buttons = event->buttons(); m_modifiers = event->modifiers(); m_accepted = false; m_inverted = event->inverted(); } qreal KirigamiWheelEvent::x() const { return m_x; } qreal KirigamiWheelEvent::y() const { return m_y; } QPointF KirigamiWheelEvent::angleDelta() const { return m_angleDelta; } QPointF KirigamiWheelEvent::pixelDelta() const { return m_pixelDelta; } int KirigamiWheelEvent::buttons() const { return m_buttons; } int KirigamiWheelEvent::modifiers() const { return m_modifiers; } bool KirigamiWheelEvent::inverted() const { return m_inverted; } bool KirigamiWheelEvent::isAccepted() { return m_accepted; } void KirigamiWheelEvent::setAccepted(bool accepted) { m_accepted = accepted; } /////////////////////////////// WheelHandler::WheelHandler(QObject *parent) : QObject(parent) { + QQuickItem *item = qobject_cast(parent); + + if (item) { + //If it's a scrollview, track its Flickable + if (item->inherits("QQuickScrollView")) { + QQuickItem *flick = item->property("contentItem").value(); + if (isFlickable(flick)) { + item = flick; + } + } + setTarget(item); + } } WheelHandler::~WheelHandler() { } QQuickItem *WheelHandler::target() const { return m_target; } void WheelHandler::setTarget(QQuickItem *target) { if (m_target == target) { return; } if (m_target) { m_target->removeEventFilter(this); } m_target = target; if (m_target) { m_target->installEventFilter(this); // Duck typing: accept everyhint that has all the properties we need m_targetIsFlickable = m_target->metaObject()->indexOfProperty("contentX") > -1 && m_target->metaObject()->indexOfProperty("contentY") > -1 && m_target->metaObject()->indexOfProperty("contentWidth") > -1 && m_target->metaObject()->indexOfProperty("contentHeight") > -1 && m_target->metaObject()->indexOfProperty("topMargin") > -1 && m_target->metaObject()->indexOfProperty("bottomMargin") > -1 && m_target->metaObject()->indexOfProperty("leftMargin") > -1 && m_target->metaObject()->indexOfProperty("rightMargin") > -1 && m_target->metaObject()->indexOfProperty("originX") > -1 && m_target->metaObject()->indexOfProperty("originY") > -1; } else { m_targetIsFlickable = false; } - - emit targetChanged(); } bool WheelHandler::eventFilter(QObject *watched, QEvent *event) { + if (!m_enabled) { + return QObject::eventFilter(watched, event); + } + if (event->type() == QEvent::Wheel) { QWheelEvent *we = static_cast(event); m_wheelEvent.initializeFromEvent(we); emit wheel(&m_wheelEvent); - if (m_scrollFlickableTarget && !m_wheelEvent.isAccepted()) { + if (m_scrollFlickableParent && !m_wheelEvent.isAccepted()) { manageWheel(we); } - if (m_blockTargetWheel) { + if (m_blockParentWheel) { return true; } } return QObject::eventFilter(watched, event); } +bool WheelHandler::isFlickable(QQuickItem *item) +{ + Q_ASSERT(item); + // Duck typing: accept everyhint that has all the properties we need + return item->metaObject()->indexOfProperty("contentX") > -1 + && item->metaObject()->indexOfProperty("contentY") > -1 + && item->metaObject()->indexOfProperty("contentWidth") > -1 + && item->metaObject()->indexOfProperty("contentHeight") > -1 + && item->metaObject()->indexOfProperty("topMargin") > -1 + && item->metaObject()->indexOfProperty("bottomMargin") > -1 + && item->metaObject()->indexOfProperty("leftMargin") > -1 + && item->metaObject()->indexOfProperty("rightMargin") > -1 + && item->metaObject()->indexOfProperty("originX") > -1 + && item->metaObject()->indexOfProperty("originY") > -1; +} + void WheelHandler::manageWheel(QWheelEvent *event) { if (!m_targetIsFlickable) { return; } qreal contentWidth = m_target->property("contentWidth").toReal(); qreal contentHeight = m_target->property("contentHeight").toReal(); qreal contentX = m_target->property("contentX").toReal(); qreal contentY = m_target->property("contentY").toReal(); qreal topMargin = m_target->property("topMargin").toReal(); qreal bottomMargin = m_target->property("bottomMargin").toReal(); qreal leftMargin = m_target->property("leftMaring").toReal(); qreal rightMargin = m_target->property("rightMargin").toReal(); qreal originX = m_target->property("originX").toReal(); qreal originY = m_target->property("originY").toReal(); // Scroll Y if (contentHeight > m_target->height()) { int y = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8; //if we don't have a pixeldelta, apply the configured mouse wheel lines if (!event->pixelDelta().y()) { y *= Settings::self()->mouseWheelScrollLines(); } // Scroll one page regardless of delta: if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) { if (y > 0) { y = m_target->height(); } else if (y < 0) { y = -m_target->height(); } } qreal minYExtent = topMargin - originY; qreal maxYExtent = m_target->height() - (contentHeight + bottomMargin + originY); m_target->setProperty("contentY", qMin(-maxYExtent, qMax(-minYExtent, contentY - y))); } //Scroll X if (contentWidth > m_target->width()) { int x = event->pixelDelta().x() != 0 ? event->pixelDelta().x() : event->angleDelta().x() / 8; // Special case: when can't scroll vertically, scroll horizontally with vertical wheel as well if (x == 0 && contentHeight <= m_target->height()) { x = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8; } //if we don't have a pixeldelta, apply the configured mouse wheel lines if (!event->pixelDelta().x()) { x *= Settings::self()->mouseWheelScrollLines(); } // Scroll one page regardless of delta: if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) { if (x > 0) { x = m_target->width(); } else if (x < 0) { x = -m_target->width(); } } qreal minXExtent = leftMargin - originX; qreal maxXExtent = m_target->width() - (contentWidth + rightMargin + originX); m_target->setProperty("contentX", qMin(-maxXExtent, qMax(-minXExtent, contentX - x))); } //this is just for making the scrollbar m_target->metaObject()->invokeMethod(m_target, "flick", Q_ARG(double, 0), Q_ARG(double, 1)); m_target->metaObject()->invokeMethod(m_target, "cancelFlick"); } WheelHandler *WheelHandler::qmlAttachedProperties(QObject *object) { return new WheelHandler(object); } #include "moc_wheelhandler.cpp" diff --git a/src/wheelhandler.h b/src/wheelhandler.h index b9b54c2c..393945a2 100644 --- a/src/wheelhandler.h +++ b/src/wheelhandler.h @@ -1,208 +1,205 @@ /* * Copyright (C) 2019 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #pragma once #include #include #include #include class QWheelEvent; /** * Describes the mouse wheel event */ class KirigamiWheelEvent : public QObject { Q_OBJECT /** * x: real * * X coordinate of the mouse pointer */ Q_PROPERTY(qreal x READ x CONSTANT) /** * y: real * * Y coordinate of the mouse pointer */ Q_PROPERTY(qreal y READ y CONSTANT) /** * angleDelta: point * * The distance the wheel is rotated in degrees. * The x and y coordinates indicate the horizontal and vertical wheels respectively. * A positive value indicates it was rotated up/right, negative, bottom/left * This value is more likely to be set in traditional mice. */ Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT) /** * pixelDelta: point * * provides the delta in screen pixels available on high resolution trackpads */ Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT) /** * buttons: int * * it contains an OR combination of the buttons that were pressed during the wheel, they can be: * Qt.LeftButton, Qt.MiddleButton, Qt.RightButton */ Q_PROPERTY(int buttons READ buttons CONSTANT) /** * modifiers: int * * Keyboard mobifiers that were pressed during the wheel event, such as: * Qt.NoModifier (default, no modifiers) * Qt.ControlModifier * Qt.ShiftModifier * ... */ Q_PROPERTY(int modifiers READ modifiers CONSTANT) /** * inverted: bool * * Whether the delta values are inverted * On some platformsthe returned delta are inverted, so positive values would mean bottom/left */ Q_PROPERTY(bool inverted READ inverted CONSTANT) /** * accepted: bool * * If set, the event shouldn't be managed anymore, * for instance it can be used to block the handler to manage the scroll of a view on some scenarions * @code * // This handler handles automatically the scroll of * // flickableItem, unless Ctrl is pressed, in this case the * // app has custom code to handle Ctrl+wheel zooming - * Kirigami.WheelHandler { - * target: flickableItem - * blockTargetWheel: true - * scrollFlickableTarget: true - * onWheel: { - * if (wheel.modifiers & Qt.ControlModifier) { - * wheel.accepted = true; - * // Handle scaling of the view - * } - * } + * Flickable { + * Kirigami.WheelHandler.enabled: true + * Kirigami.WheelHandler.onWheel: { + * if (wheel.modifiers & Qt.ControlModifier) { + * wheel.accepted = true; + * // Handle scaling of the view + * } + * } * } * @endcode * */ Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted) public: KirigamiWheelEvent(QObject *parent = nullptr); ~KirigamiWheelEvent(); void initializeFromEvent(QWheelEvent *event); 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); private: qreal m_x = 0; qreal m_y = 0; QPointF m_angleDelta; QPointF m_pixelDelta; Qt::MouseButtons m_buttons = Qt::NoButton; Qt::KeyboardModifiers m_modifiers = Qt::NoModifier; bool m_inverted = false; bool m_accepted = false; }; /** - * 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 + * This class intercepts the mouse wheel events of its parent, 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 parent, and if the parent is a Flickable, it can automatically handle scrolling on it */ class WheelHandler : public QObject { Q_OBJECT /** - * target: Item - * - * 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. + * enabled: bool + * + * If true it will fiter wheel events of its parent item */ - Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged) + Q_PROPERTY(bool enabled MEMBER m_enabled NOTIFY enabledChanged) /** - * blockTargetWheel: bool + * blockParentWheel: bool * * If true, the target won't receive any wheel event at all (default true) */ - Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged) + Q_PROPERTY(bool blockParentWheel MEMBER m_blockParentWheel NOTIFY blockParentWheelChanged) /** - * scrollFlickableTarget: bool + * scrollFlickableParent: bool * If this property is true and the target is a Flickable, wheel events will cause the Flickable to scroll (default true) */ - Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged) + Q_PROPERTY(bool scrollFlickableParent MEMBER m_scrollFlickableParent NOTIFY scrollFlickableParentChanged) public: explicit WheelHandler(QObject *parent = nullptr); ~WheelHandler() override; QQuickItem *target() const; void setTarget(QQuickItem *target); //QML attached property static WheelHandler *qmlAttachedProperties(QObject *object); protected: bool eventFilter(QObject *watched, QEvent *event) override; Q_SIGNALS: - void targetChanged(); - void blockTargetWheelChanged(); - void scrollFlickableTargetChanged(); + void enabledChanged(); + void blockParentWheelChanged(); + void scrollFlickableParentChanged(); void wheel(KirigamiWheelEvent *wheel); private: + inline bool isFlickable(QQuickItem *item); void manageWheel(QWheelEvent *wheel); QPointer m_target; - bool m_blockTargetWheel = true; - bool m_scrollFlickableTarget = true; + bool m_enabled = false; + bool m_blockParentWheel = true; + bool m_scrollFlickableParent = true; bool m_targetIsFlickable = false; KirigamiWheelEvent m_wheelEvent; }; QML_DECLARE_TYPEINFO(WheelHandler, QML_HAS_ATTACHED_PROPERTIES) diff --git a/tests/WheelHandler.qml b/tests/WheelHandler.qml new file mode 100644 index 00000000..2f085b3c --- /dev/null +++ b/tests/WheelHandler.qml @@ -0,0 +1,69 @@ +/* + * Copyright 2016 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.3 as Controls +import org.kde.kirigami 2.9 as Kirigami + +Controls.ScrollView { + id: root + width: Kirigami.Units.gridUnit * 30 + height: Kirigami.Units.gridUnit * 40 + + readonly property Flickable flickable: contentItem + + Kirigami.WheelHandler.enabled: true + Kirigami.WheelHandler.onWheel: { + if (wheel.modifiers & Qt.ControlModifier) { + wheel.accepted = true; + var factor = 1.2; + + // Shrink + if (wheel.angleDelta.y < 0 || wheel.pixelDelta.y < 0) { + factor = 0.83 + } + + contents.zoom = Math.max(contents.zoom * factor, 1); + flickable.resizeContent(contents.implicitWidth , contents.implicitHeight, contents.mapFromItem(flickable, wheel.x, wheel.y)); + flickable.contentWidth = contents.implicitWidth; + flickable.contentHeight = contents.implicitHeight; + + flickable.returnToBounds(); + } + } + + Item { + id: contents + + property real zoom: 1 + implicitWidth: root.width * zoom + implicitHeight: Kirigami.Units.gridUnit * 60 * zoom + Rectangle { + anchors { + fill: parent + margins: Kirigami.Units.gridUnit * 2 + } + color: "red" + Controls.Label { + anchors.centerIn: parent + text: contents.width+"x"+contents.height + } + } + } +}