How can I propagate hover or mousearea update events to lower elements in QML?
Asked Answered
R

3

3

I have some sibling Rectangle elements with a radius property so they appear as circles. Each has a child Item that has a child MouseArea, the purpose of the Item being to implement a "round mouse area" effect (original SO answer). The Item and MouseArea are instrumented such that clicks and drags will only take effect within the visible circular shape of the Rectangle, not within the bounding box that is the real footprint of the Rectangle.

Unfortunately there is a glitch illustrated below. The desired outcome when dragging at the dot is for circle 1 to move, and this happens in most circumstances. However, it does not happen when you create create circle 1 then circle 2 then move your mouse cursor to the dot. If you do that and attempt to drag or click, your interaction will fall through to the background full-window MouseArea and create a new circle.

two circles partially overlapping, numbered 1 and 2, with a dot in one circle near the other

The cause of this problem is that when the mouse cursor moves to the dot from circle #2, the mouseX and mouseY for circle #1's MouseArea do not get updated. When circle #2 allows the click to propagate downward, it hits the Rectangle of circle #1 but then circle #1's Item claims containsMouse is false and it propagates downward again.

As soon as the mouse cursor leaves the footprint of circle #2's bounding rectangle, such as by moving a bit up or left from the dot, circle #1's MouseArea gets updated and its containsMouse becomes true and it starts capturing clicks and drags again.

I have tried a handful of potential solutions and not gotten much farther than the code below.

import QtQuick 2.12
import QtQuick.Controls 2.5

ApplicationWindow {
    visible: true
    width: 640
    height: 480

    property real spotlightRadius: 100

    MouseArea {
        visible: true
        anchors.fill: parent
        onClicked: {
            spotlightComponent.createObject(parent, {
                "x": x + mouseX - spotlightRadius,
                "y": y + mouseY - spotlightRadius,
                "width": spotlightRadius * 2,
                "height": spotlightRadius * 2
            })
        }
    }

    Component {
        id: spotlightComponent
        Rectangle {
            id: spotlightCircle
            visible: true
            x: parent.x
            y: parent.y
            width: parent.width
            height: parent.height
            radius: Math.max(parent.width, parent.height) / 2
            color: Qt.rgba(Math.random()*0.5+0.5,Math.random()*0.5+0.5,Math.random()*0.5+0.5,0.5);
            Item {
                anchors.fill: parent
                drag.target: parent
                onDoubleclicked: parent.destroy()
                onWheel: { parent.z += wheel.pixelDelta.y; currentSpotlight = parent }

                property alias drag: mouseArea.drag

                //FIXME when moving the mouse out of a higher element's containsMouse circle
                // but still inside its mouseArea.containsMouse square, lower elements'
                // mouseArea do not update, so their containsMouse doesn't update, so clicks
                // fall through when they should not.
                property bool containsMouse: {
                    var x1 = width / 2;
                    var y1 = height / 2;
                    var x2 = mouseArea.mouseX;
                    var y2 = mouseArea.mouseY;
                    var deltax = x1 - x2;
                    var deltay = y1 - y2;
                    var distance2 = deltax * deltax + deltay * deltay;
                    var radius2 = Math.pow(Math.min(width, height) / 2, 2);
                    return distance2 < radius2;
                }

                signal clicked(var mouse)
                signal doubleclicked(var mouse)
                signal wheel(var wheel)

                MouseArea {
                    id: mouseArea
                    anchors.fill: parent
                    hoverEnabled: true
                    //FIXME without acceptedButtons, propagated un-accepted clicks end up with the wrong coordinates
                    acceptedButtons: parent.containsMouse ? Qt.LeftButton : Qt.NoButton
                    propagateComposedEvents: true
                    onClicked: { if (parent.containsMouse) { parent.clicked(mouse) } else { mouse.accepted = false } }
                    onDoubleClicked: { if (parent.containsMouse) { parent.doubleclicked(mouse) } }
                    onWheel: { if (parent.containsMouse) { parent.wheel(wheel) } }
                    drag.filterChildren: true
                }
            }
        }
    }
}
Rather answered 18/12, 2018 at 4:0 Comment(1)
Maybe try DragHandler, see if that helps?Dinodinoflagellate
C
3

I think that using HoverHandler instead of MouseArea can give the desired result in this case because they stack as you'd expect i.e. they are full "transparent" for mouse movement, nor are they blocked by any MouseArea that happens to be on top.

Causation answered 29/12, 2020 at 14:12 Comment(0)
L
2

This is not the exact solution for your problem, but this is how I overcame the root of the issue.

In my application there is a MouseArea that overlaps a large chunk of the scene which is a QQuickFrameBufferObject. This is where I draw the 3D scene. Since you cannot propagate a QHoverEvent in QML, you will have to catch the position changed signal using the onPositionChanged handler and invoke a method in C++ which will send a QHoverEvent to the required items.

QML:

MouseArea {
    onPositionChanged: {
        model.sendHoverEvent(Qt.point(mouse.x, mouse.y))
    }
}

C++:

class TreeViewModel : public QAbstractListModel
{
    // ...
    void TreeViewModel::sendHoverEvent(QPointF p) {
        QHoverEvent hoverEvent(QEvent::HoverMove, p, p);
        QApplication::sendEvent(mApplication.graphicsLayer(), &hoverEvent);
    }
};
Linkwork answered 9/10, 2019 at 11:54 Comment(0)
T
0

You need to reject the pressed event of your underlying MouseArea. It should be enough to solve your problems. If the pressed event is rejected, the click will automatically be forwarded to the underlying sibling items. propagateComposedEvents and filterChildren are useless in your case.

Note that if the wheel event causes the z coordinate of your spotlightCircle to become less than 0, it will no longer accept mouse event since they will be caught by the "Creation" MouseArea

import QtQuick 2.10
import QtQuick.Controls 2.3

ApplicationWindow {
    visible: true
    width: 640
    height: 480

    property real spotlightRadius: 100

    MouseArea {
        visible: true
        anchors.fill: parent
        onClicked: {
            spotlightComponent.createObject(parent, {
                "x": x + mouseX - spotlightRadius,
                "y": y + mouseY - spotlightRadius,
                "width": spotlightRadius * 2,
                "height": spotlightRadius * 2
            })
        }
    }

    Component {
        id: spotlightComponent
        Rectangle {
            id: spotlightCircle
            visible: true
            x: parent.x
            y: parent.y
            width: parent.width
            height: parent.height
            radius: Math.max(parent.width, parent.height) / 2
            color: Qt.rgba(Math.random()*0.5+0.5,Math.random()*0.5+0.5,Math.random()*0.5+0.5,0.5);
            Item {
                anchors.fill: parent
                onDoubleClicked: parent.destroy()
                onWheel: { parent.z += wheel.pixelDelta.y; currentSpotlight = parent }

                signal clicked(var mouse)
                signal pressed(var mouse)
                signal doubleClicked(var mouse)
                signal wheel(var wheel)
                property alias drag: mouseArea.drag
                property bool containsMouse: {
                    var x1 = width / 2;
                    var y1 = height / 2;
                    var x2 = mouseArea.mouseX;
                    var y2 = mouseArea.mouseY;
                    var deltax = x1 - x2;
                    var deltay = y1 - y2;
                    var distance2 = deltax * deltax + deltay * deltay;
                    var radius2 = Math.pow(Math.min(width, height) / 2, 2);
                    return distance2 < radius2;
                }

                MouseArea {
                    id: mouseArea
                    anchors.fill: parent
                    hoverEnabled: true
                    drag.target: spotlightCircle
                    onPressed: { if (parent.containsMouse) { parent.pressed(mouse) } else { mouse.accepted = false } }
                    onClicked: { if (parent.containsMouse) { parent.clicked(mouse) } else { mouse.accepted = false } }
                    onDoubleClicked: { if (containsMouse2) { parent.doubleClicked(mouse) } }
                    onWheel: { if (parent.containsMouse) { parent.wheel(wheel) } }
                }
            }

        }
    }
}
Transient answered 18/12, 2018 at 9:8 Comment(2)
I cannot test right now but will say that the problem I had when I tried this approach was that even when the click/press/drag/etc events get to the underlying element, that element's MouseArea's coordinates haven't been updated so it thinks its containsMouse is false.Rather
Works on my machine (not a very valid point though...). Maybe some slight behavior change between QtQuick 2.10 and 2.12? Anyway, a possible workaround might be to change the containsMouse property into a callable function, which takes the mouse event as parameter. Thus, you will be sure to get the right coordinatesTransient

© 2022 - 2024 — McMap. All rights reserved.