1

In MouseArea.onEntered, can I detect if the cause for the event firing is only that the MouseArea moved and came to be under the cursor, rather than the other way round?

I thought of doing something like this: (pseudocode)

    MouseArea {
        // ...
        property bool positionDirty = false
        Connections {
            target: window
            onAfterRendering: {
                positionDirty = false;
            }
        }
        onMouseAreaPosChanged: {
            positionDirty = true;
        }
        onEntered: {
            if(positionDirty) {
                positionDirty = false;
                return;
            }
            // handle event here
        }
    }

But this makes the assumption that entered will be fired after mouseAreaPosChanged, and before window.afterRendering. And I'm not confident in that assumption.

Also, it doesn't work when an ancestor of the MouseArea moves, or when the MouseArea is positioned/sized via anchoring.

Stefan Monov
  • 11,332
  • 10
  • 63
  • 120
  • What is the supposed outcome, if both, mouse and `MouseArea` are moving, and that causes the collision? And how precise do you need to be? Won't a timer with a few ms do the job? – derM - not here for BOT dreams May 09 '17 at 14:06
  • It will be very tricky, if it is only allowed by not-moving mouses, because a non-moving mouse won't enter a `MouseArea` at all. – derM - not here for BOT dreams May 09 '17 at 14:30
  • @derM: Re: "if both are moving": That's would be a rare edge case in my app, but anyway, the expected result is that I do handle the event as if the MouseArea hadn't moved. It doesn't need to be very precise I think, but it does need to be reliable. So I don't think a timer would work for me. E.g. if the timer's `onTriggered` handler sets `positionDirty = false` but does so a bit too early, I may end up handling onEntered as if it was caused by a mouse movement. – Stefan Monov May 09 '17 at 14:44
  • @derM: I don't understand your second comment at all. How does "a non-moving mouse won't enter a MouseArea at all" mean that my task is tricky? – Stefan Monov May 09 '17 at 14:46
  • If you have the `MousePointer` not moving, and the `MouseArea` moves in a way, that it now contains the `MousePointer`, the `MouseArea.containsMouse` won't change. This per se is not even the biggest problem. The most problematic is, to detect, whether the mouse *is still in the MouseArea, when it entered the `MouseArea` by mouse movement, and leaves it, due tu movement of the `MouseArea`* See my example code, that detects the entering, but has issues with the leaving. – derM - not here for BOT dreams May 09 '17 at 15:09
  • I'm not sure if this helps you, but there was a issue that sounds related to this that was fixed recently: https://bugreports.qt.io/browse/QTBUG-42194 https://codereview.qt-project.org/#/c/127724/ – Mitch May 10 '17 at 07:59
  • @Mitch: Thank you for pointing me to this. This would have broken my solution. – derM - not here for BOT dreams May 10 '17 at 09:27

2 Answers2

2

Assumptions:

This only affects the edge case, that both, the cursor and the MouseArea are moving.
My Assumption here is, that the movement of the cursor is handled before the movement of the MouseArea. I don't have any definite proof for this. Only my test with the solution below, suggests that.


Solution

The first challenge is to detect movement of the MouseArea. It might be that it moves, without its own x and y-values changing, e.g. if its parent is moving.
To solve this, I'll introduce two properties globalX and globalX. Then I use the trick from this answer on how to track a gobal position of an object.

Now I'll have two signals to handle: globalXChanged and globalYChanged.
According to my assumption, they are fired, after the mouseXChanged and mouseYChanged. I will use a flag isEntered to make sure, I only handle one of them, by setting it to true, if the first of them is fired.

I will use the cursor position on a globalMouseArea to determine, whether the cursor is within bounds of the MouseArea. This requires, the cursor is not in some other MouseArea at that time, or at least I know of it

-> With this I already succeeded in detecting the entrance.


The second challenge is to detect the exit. Here we have 4 cases to distinguish:

  1. Cursor enters and leaves the MouseArea because of it's movement.
  2. Cursor enters and leaves the MouseArea because of the movement of the MouseArea
  3. Cursor enters because the MouseArea moves, and leaves, because the cursor moves
  4. Cursor enters because it moves, and leaves as the MouseArea moves away.

The first would be easy to handle. After it enters we handle entered and when it leaves we handle exited. But after the fix, mentioned by Mitch we can't rely on this anymore.

So we will not set hoverEnabled: true and map the position of the cursor to the targetMouseArea whenever either the cursor moves, or the targetMouseArea moves, and act accordingly.

import QtQuick 2.7
import QtQuick.Controls 2.0

ApplicationWindow {
    id: root
    visible: true
    width: 400; height: 450
    MouseArea {
        id: globalMouseArea
        anchors.fill: parent
        hoverEnabled: true
        onClicked: ani.restart()
    }

    Rectangle {
        x: 300
        y: 300
        width: 50
        height: 50
        color: 'green'
    }

    Rectangle {
        id: rect
        width: 50
        height: 50
        color: 'red'

        Text {
            text: targetMouseArea.isEntered.toString()
        }

        MouseArea {
            id: targetMouseArea
            anchors.fill: parent
            signal enteredBySelfMovement
            signal enteredByMouseMovement

            onEnteredByMouseMovement: console.log('Cause: Mouse')
            onEnteredBySelfMovement: console.log('Cause: Self')

            property point globalPos: {
                var c = Qt.point(0, 0)
                var itm = this
                for (; itm.parent !== null; itm = itm.parent) {
                    c.x += itm.x
                    c.y += itm.y
                }
                return c
            }
            property bool isEntered: false

            function checkCollision(sig) {
                if ((globalPos.y < globalMouseArea.mouseY)
                    && (globalPos.y + height > globalMouseArea.mouseY)
                    && (globalPos.x < globalMouseArea.mouseX)
                    && (globalPos.x + width > globalMouseArea.mouseX)) {
                    if (!isEntered) { 
                        isEntered = true
                        sig()
                    }
                }
                else if (isEntered && !containsMouse) {
                    console.log(isEntered = false)
                }
            }

            onGlobalPosChanged: {
                checkCollision(enteredBySelfMovement)
            }

            Connections {
                target: globalMouseArea
                onPositionChanged: {
                    targetMouseArea.checkCollision(targetMouseArea.enteredByMouseMovement)
                }
            }
        }
    }

    NumberAnimation {
        id: ani
        target: rect
        properties: 'x,y'
        from: 0
        to: 300
        running: true
        duration: 10000
    }
}

Problems left: When we clicked within the targetMouseArea, as long as a button is pressed, we won't detect the leaving.

Community
  • 1
  • 1
  • Thanks. Your code seems to rely on onGlobal*Changed being called *before* onEntered. But will that always be true? – Stefan Monov May 09 '17 at 15:30
  • "If onGlobalChanged would be handled before, I could not detect an entrance by mouse movement at all" - but if onGlobalChanged is called, leading to `isEntered = true`, then I don't **want** to detect an entrance by mouse movement, because there isn't any such entrance - we've already entered via item movement. – Stefan Monov May 09 '17 at 15:57
  • In the case when only the item moves, both onGlobalChanged and onEntered will be called. And, in that case, if onEntered is called before onGlobalChanged, we still won't have assigned `isEntered = true`, so `enteredByMouseMovement()` will be called, **which I don't want**. – Stefan Monov May 09 '17 at 16:04
  • I forgot to include the *first fix* in my code. Therefore, when the mouse entered by mouse movement, the `isEntered` has been resetted to `false` on the next positon update of the `targetMouseArea`, and then it was detected that is inside, so the `enteredBySelfMovement` was fired. – derM - not here for BOT dreams May 10 '17 at 08:26
  • Ok, now I think I provided extensive explaination and a working example for my idea. I hope you can follow my thought. – derM - not here for BOT dreams May 10 '17 at 09:11
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/143893/discussion-between-stefan-monov-and-derm). – Stefan Monov May 10 '17 at 15:19
  • First you say it makes sense, then you find issues with it? Seems like you're contradicting yourself ;) . Anyway, what you say is premature optimization and would lead to an extremely insignificant gain. What I did is standard refactoring that people do all the time. Anyway, I like your latest solution a bit less than mine (in terms of readability), but if you insist on it, I won't argue :) – Stefan Monov May 15 '17 at 15:03
  • Ok... I meant: What you did with *merging posX and posY into a single property* and so on makes sense :D - From the point of readability: I don't know. From the point of [*Linus Torvalds*](https://en.wikiquote.org/wiki/Linus_Torvalds#1995-99) you are right. From the siginificance of the gain: I have not measured it. – derM - not here for BOT dreams May 15 '17 at 15:19
0

You can check whether mouseX, mouseY were changed since last event.

property int previousMouseX = mouseX; // or use other values to init
property int previousMouseY = mouseY; // e.g., 0, x, parent.x, 
                                      // or set it from extern

onEntered() {
    if (mouseX != previousMouseX || mouseY != previousMouseY) {
       // TODO do something
        previousMouseX = mouseX;
        previousMouseY = mouseY;
    }
}

In case mouseX, mouseY are relative to the mouse area 0,0 you can use mapFromItem(null, 0, 0) to get the absolute values.

Th. Thielemann
  • 2,592
  • 1
  • 23
  • 38
  • 1
    This won't work. If the mouse enters for the first time, e.g. it is totaly unknown. Otherwise we'll just know, whether the mouse has crossed the border at the same place as before, but we don't know the reason. – derM - not here for BOT dreams May 09 '17 at 14:05
  • You can use another value to init the previous position. Eg. previousMouseX = x or previousMouseX = 0 or set it from extern. – Th. Thielemann May 10 '17 at 07:13