godot icon indicating copy to clipboard operation
godot copied to clipboard

"mouse_entered" and "mouse_exited" signals are broken while holding down a mouse button

Open PostPollux opened this issue 6 years ago • 31 comments

Godot version: fbb5ca4

OS/device including version: Linux Manjaro

Issue description: The "mouse_entered" and "mouse_exited" signals of control nodes don't get fired while holding any mouse button down when the click (any mouse button down) started on a control that does not ignore the mouse.

it's actually because in viewport.cpp in line 1843 to 1850 "over" get's set to the mouse_focus

if (gui.mouse_focus) {
	over = gui.mouse_focus;
	//recompute focus_inv_xform again here
} else {
	over = _gui_find_control(mpos);
}

and shortly after ( line 1884 to 1893 ) the mouse_exited and mouse_entered signals get not fired because the gui.mouse_focus and the gui.mouse_over are the same

if (over != gui.mouse_over) {

	if (gui.mouse_over)
		gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT);

	_gui_cancel_tooltip();

	if (over)
		over->notification(Control::NOTIFICATION_MOUSE_ENTER);
}

This may be a desired behavior, for a lot of cases. But there are also cases where it is very useful to send those signals. (I'll update this post soon to show such a case)

Steps to reproduce:

  1. Connect the "mouse_entered" or "mouse_exited" signal of any control node
  2. add another control node, that does not ignore the mouse
  3. start your drag on the control element of point 2 and drag over the control element from point 1
  4. notice that no "mouse_entered" and no "mouse_exited" signals get fired

PostPollux avatar Aug 10 '18 11:08 PostPollux

Confirmed on my master build too. Note: if you set mouse filter to "ignore" on the parent control the color rectangle signals works as expected.

spongeboburu avatar Aug 10 '18 12:08 spongeboburu

linked to #23296

jeremyz avatar Feb 07 '19 13:02 jeremyz

Note from #32727, this does also happen for is_hovered()

groud avatar Oct 10 '19 20:10 groud

Godot版本: fbb5ca4

操作系统/设备,包括版本: Linux Manjaro

问题描述: 当在不忽略鼠标的控件上单击(按下任何鼠标按钮)时,按住任意鼠标按钮时,不会触发控制节点的“ mouse_entered”和“ mouse_exited”信号。

实际上是因为在1843至1850行的viewport.cpp中,“ over”被设置为mouse_focus

if (gui.mouse_focus) {
	over = gui.mouse_focus;
	//recompute focus_inv_xform again here
} else {
	over = _gui_find_control(mpos);
}

并且在之后(第1884至1893行)不久,由于gui.mouse_focus和gui.mouse_over相同,因此未触发mouse_exited和mouse_entered信号

if (over != gui.mouse_over) {

	if (gui.mouse_over)
		gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT);

	_gui_cancel_tooltip();

	if (over)
		over->notification(Control::NOTIFICATION_MOUSE_ENTER);
}

在很多情况下,这可能是理想的行为。 但是在某些情况下,发送这些信号非常有用。(我将尽快更新此帖子以显示这种情况)

重现步骤:

  1. 连接任何控制节点的“ mouse_entered”或“ mouse_exited”信号
  2. 添加另一个不忽略鼠标的控制节点
  3. 在点2的控件元素上开始拖动,然后从点1的控件元素上拖动
  4. 请注意,没有触发“ mouse_entered”和“ mouse_exited”信号

how solve it...cry... #35262

yuyubibibobibo avatar Jan 20 '20 02:01 yuyubibibobibo

Still an issue in Godot 3.2.2beta ed27b7e6b973eb6be6fdac4439e99a1907b9cb58

swenner avatar Apr 24 '20 12:04 swenner

Still in 3.2.4RC2. Try to click on bottom element. With out releasing it move cursor away from element! New Game Project.zip UPD Works with some fixes from code. https://github.com/godotengine/godot/issues/20881#issuecomment-822360392

MadDogMayCry0 avatar Feb 24 '21 16:02 MadDogMayCry0

managed to get around this by making my own _input(event:InputEventMouseButton) function. I then had to make sure these two lines were executed as part of the code when the mouse is clicked down:

event.set_pressed(false)
set("mouse_filter", Control.MOUSE_FILTER_STOP)

I then reset the mouse_filter to ..._PASS on the mouse release. This only worked with an _input() function, not a _gui_input() function

MrJoshBowman avatar Apr 14 '21 03:04 MrJoshBowman

FIX :) Mouse pressed signals bug.zip

Can not get this to work. The line get_global_rect().has_point(get_global_mouse_position()) does not take into account either scale or rotation of the node :(

Banderi avatar May 11 '21 11:05 Banderi

FIX :) Mouse pressed signals bug.zip

Can not get this to work. The line get_global_rect().has_point(get_global_mouse_position()) does not take into account either scale or rotation of the node :(

get_global_rect().has_point(get_local_mouse_position())

MadDogMayCry0 avatar May 11 '21 21:05 MadDogMayCry0

FIX :) Mouse pressed signals bug.zip

Can not get this to work. The line get_global_rect().has_point(get_global_mouse_position()) does not take into account either scale or rotation of the node :(

get_global_rect().has_point(get_local_mouse_position())

Both get_local_mouse_position or get_global_mouse_position do not work. As I said, the Rect2 that get_global_rect returns does not take into account rotation or scale.

EDIT: Actually, here's what worked for me:

var rect_check = Rect2(Vector2(0,0), node.get_global_rect().size)
var local_mouse_pos = node.get_local_mouse_position()
if rect_check.has_point(local_mouse_pos):
	inside = true

get_local_mouse_position does indeed take into account rotation and scale, as it's in local space. Just had to check it against the local sizes of the Rect2 instead of the global ones.

Banderi avatar May 12 '21 02:05 Banderi

@Banderi Local mouse position, rotating, scaling and pivot :D Mouse pressed signals bug.zip

MadDogMayCry0 avatar May 15 '21 07:05 MadDogMayCry0

Wasted hours thinking it was my code :-( v3.3.1 Appreciate the work around but hoping this gets a fix/resolution

deanl42 avatar Jun 22 '21 07:06 deanl42

This is probably related to a common UI behavior, which I saw called "mouse capture" on Windows. The idea is to make it easier to implement exclusive dragging logic, by making the control receive mouse events and appear pressed as long as the mouse was pressed inside and remains pressed even if it exits the area of the control. I would not be surprised if this wasnt documented because it's a subtle behavior happening with many widgets, and has no associated option.

For example, take a scrollbar. Press it, keep the mouse held, and move away from it. It will remain pressed, and will still respond to your movements, while every other control will ignore the mouse. You can reproduce this behavior on non-Godot UIs as well, even sometimes on controls that are not usually draggable (see how some buttons remain pressed when you do this too). Things can differ a bit though, because there are many UI systems around. Sometimes hover effects don't follow this, while pressed effects do. Godot happens to do both, including enter/exit.

About mouse_entered and mouse_exited: arguably, many widgets probably don't need mouse capture. But if these were changed to always fire when the mouse goes in and out of the control's area, regardless of what's actually happening, then likely a bunch of existing code might need to be fixed somehow (if they rely on it), without being able to rely on them. Or maybe controls can have an option to turn off mouse capture.

Zylann avatar Jun 23 '21 17:06 Zylann

This is probably related to a common UI behavior, which I saw called "mouse capture" on Windows. The idea is to make it easier to implement exclusive dragging logic, by making the control receive mouse events and appear pressed as long as the mouse was pressed inside and remains pressed even if it exits the area of the control. I would not be surprised if this wasnt documented because it's a subtle behavior happening with many widgets, and has no associated option.

For example, take a scrollbar. Press it, keep the mouse held, and move away from it. It will remain pressed, and will still respond to your movements, while every other control will ignore the mouse. You can reproduce this behavior on non-Godot UIs as well, even sometimes on controls that are not usually draggable (see how some buttons remain pressed when you do this too). Things can differ a bit though, because there are many UI systems around. Sometimes hover effects don't follow this, while pressed effects do. Godot happens to do both, including enter/exit.

About mouse_entered and mouse_exited: arguably, many widgets probably don't need mouse capture. But if these were changed to always fire when the mouse goes in and out of the control's area, regardless of what's actually happening, then likely a bunch of existing code might need to be fixed somehow, without being able to rely on them. Or maybe controls can have an option to turn off mouse capture.

Thanks for that, makes sense. If this is the case then I would def upvote an option to switch it off for certain scenarios.

deanl42 avatar Jun 25 '21 06:06 deanl42

Out of curiosity, does this behavior happen with CollisionObject2D/3D's mouse_enter/mouse_exit signals too?

Thanks for that, makes sense. If this is the case then I would def upvote an option to switch it off for certain scenarios.

Feel free to open a pull request to do this :slightly_smiling_face:

The property should be added to Control and the default behavior should be kept as it is now.

Calinou avatar Jul 21 '21 01:07 Calinou

This is also a huge problem for gui_input(event) signals from any Control based nodes.

For example when trying to trigger an animation when the left-mouse is released over the Control:

func _on_Control_gui_input(event):
	if event.is_action_released("LMB"):
		$AnimationPlayer.play("New Anim")

This should trigger the animation only when the left mouse is released over the Control, but if clicked onto the Control then dragged the mouse away from the control (while still being pressed) the animation will play, regardless of where the mouse is released.

This seems to be connected to the mouse_enter and mouse_exit signal, as can be easily tested with this:

var mouse_hover = false

func _on_Control_gui_input(event):
	if event.is_action_released("LMB") and mouse_hover:
		$AnimationPlayer.play("New Anim")
		print("gui ",mouse_hover)

func _on_Control_mouse_entered():
	mouse_hover = true
	print("mouse_hover = true")

func _on_Control_mouse_exited():
	mouse_hover = false
	print("mouse_hover = false")

Both "gui True" and "mouse_hover = false" will print only after mouse release.

golddotasksquestions avatar Aug 16 '21 11:08 golddotasksquestions

This is also a huge problem for gui_input(event) signals from any Control based nodes.

For example when trying to trigger an animation when the left-mouse is released over the Control:

func _on_Control_gui_input(event):
	if event.is_action_released("LMB"):
		$AnimationPlayer.play("New Anim")

This should trigger the animation only when the left mouse is released over the Control, but if clicked onto the Control then dragged the mouse away from the control (while still being pressed) the animation will play, regardless of where the mouse is released.

This seems to be connected to the mouse_enter and mouse_exit signal, as can be easily tested with this:

var mouse_hover = false

func _on_Control_gui_input(event):
	if event.is_action_released("LMB") and mouse_hover:
		$AnimationPlayer.play("New Anim")
		print("gui ",mouse_hover)

func _on_Control_mouse_entered():
	mouse_hover = true
	print("mouse_hover = true")

func _on_Control_mouse_exited():
	mouse_hover = false
	print("mouse_hover = false")

Both "gui True" and "mouse_hover = false" will print only after mouse release.

https://github.com/godotengine/godot/issues/20881#issuecomment-841614529

MadDogMayCry0 avatar Aug 16 '21 13:08 MadDogMayCry0

One workaround is to toggle the visibility of the clicked Control immediately after being pressed:

func _gui_input(event):
	if event is InputEventMouseButton and event.pressed:
		visible = false
		visible = true

Another hacky workaround:

get_viewport().notification(NOTIFICATION_WM_FOCUS_OUT)

volkathemartian avatar Oct 11 '21 20:10 volkathemartian

@volkathemartian How is this workaround supposed to be implemented?

I tried it with my example above, and it does not work:

var mouse_hover = false

func _on_Control_gui_input(event):
	if event is InputEventMouseButton and event.pressed:
		visible = false
		visible = true
	if event.is_action_released("LMB") and mouse_hover:
		$AnimationPlayer.play("New Anim")
		print("gui ",mouse_hover)

func _on_Control_mouse_entered():
	mouse_hover = true
	print("mouse_hover = true")

func _on_Control_mouse_exited():
	mouse_hover = false
	print("mouse_hover = false")

If I click and release directly on the Control, turning visibility on and off results only in the Control triggering the mouse_entered signal again, but this will also eat up the event.is_action_released() test for some reason. The animation never plays and neither does it print "gui ...".

Here is my test project if you want to try for yourself: Control_click_drag_release_issue.zip Please let me know if you find a working workaround, this bug is driving me nuts.

golddotasksquestions avatar Oct 18 '21 08:10 golddotasksquestions

One workaround is to toggle the visibility of the clicked Control immediately after being pressed:

func _gui_input(event):
	if event is InputEventMouseButton and event.pressed:
		visible = false
		visible = true

Another hacky workaround:

get_viewport().notification(NOTIFICATION_WM_FOCUS_OUT)

Did you check it yourself? I'm not sure if what you posted is a solution to the problem!

MadDogMayCry0 avatar Oct 18 '21 09:10 MadDogMayCry0

@volkathemartian How is this workaround supposed to be implemented?

I tried it with my example above, and it does not work:

var mouse_hover = false

func _on_Control_gui_input(event):
	if event is InputEventMouseButton and event.pressed:
		visible = false
		visible = true
	if event.is_action_released("LMB") and mouse_hover:
		$AnimationPlayer.play("New Anim")
		print("gui ",mouse_hover)

func _on_Control_mouse_entered():
	mouse_hover = true
	print("mouse_hover = true")

func _on_Control_mouse_exited():
	mouse_hover = false
	print("mouse_hover = false")

If I click and release directly on the Control, turning visibility on and off results only in the Control triggering the mouse_entered signal again, but this will also eat up the event.is_action_released() test for some reason. The animation never plays and neither does it print "gui ...".

Here is my test project if you want to try for yourself: Control_click_drag_release_issue.zip Please let me know if you find a working workaround, this bug is driving me nuts.

He probably forgot to add that this is his theory. Try my attachments, there is the only working option at the moment. https://github.com/godotengine/godot/files/6483692/Mouse.pressed.signals.bug.zip

MadDogMayCry0 avatar Oct 18 '21 09:10 MadDogMayCry0

This is probably related to a common UI behavior, which I saw called "mouse capture" on Windows. The idea is to make it easier to implement exclusive dragging logic, by making the control receive mouse events and appear pressed as long as the mouse was pressed inside and remains pressed even if it exits the area of the control. I would not be surprised if this wasnt documented because it's a subtle behavior happening with many widgets, and has no associated option.

For example, take a scrollbar. Press it, keep the mouse held, and move away from it. It will remain pressed, and will still respond to your movements, while every other control will ignore the mouse. You can reproduce this behavior on non-Godot UIs as well, even sometimes on controls that are not usually draggable (see how some buttons remain pressed when you do this too). Things can differ a bit though, because there are many UI systems around. Sometimes hover effects don't follow this, while pressed effects do. Godot happens to do both, including enter/exit.

About mouse_entered and mouse_exited: arguably, many widgets probably don't need mouse capture. But if these were changed to always fire when the mouse goes in and out of the control's area, regardless of what's actually happening, then likely a bunch of existing code might need to be fixed somehow (if they rely on it), without being able to rely on them. Or maybe controls can have an option to turn off mouse capture.

This has nothing to do with mouse behavior in Windows.

MadDogMayCry0 avatar Oct 18 '21 09:10 MadDogMayCry0

This has nothing to do with mouse behavior in Windows.

What do you mean? This behaviour is consistent across a whole lot of the programs I use on linux, and I imagine it is the same on Windows.

If I click something that does not have drag-and-drop functionality in a window (any application, not just godot ones) and then move the mouse elsewhere in the window (or even to other windows) with the button down, almost without exception there will be no indication the mouse hovers over anything until the button is released.

Meriipu avatar Oct 18 '21 09:10 Meriipu

This has nothing to do with mouse behavior in Windows.

What do you mean? This behaviour is consistent across a whole lot of the programs I use on linux, and I imagine it is the same on Windows.

If I click something that does not have drag-and-drop functionality in a window (any application, not just godot ones) and then move the mouse elsewhere in the window (or even to other windows) with the button down, almost without exception there will be no indication the mouse hovers over anything until the button is released.

I mean exactly what I mean - the interpretation of the cursor behavior within the godot's engine logic has nothing to do with the interpretation and behavior of the mouse anywhere, even in Windows, even in Linux, even on an alien spaceship.

MadDogMayCry0 avatar Oct 18 '21 09:10 MadDogMayCry0

@MadDogMayCry0 I can't get your workaround to work with my gui_input(event) example. I suppose it would be possible if I copy it line by line, use _input(event) instead of gui_input(event) and separate scripts like you did, but this is way too complicated for what I want to do.

golddotasksquestions avatar Oct 18 '21 11:10 golddotasksquestions

@MadDogMayCry0 even though it works, what you do with if event.is_pressed(): event.pressed = false is to by pass the code that starts here : https://github.com/godotengine/godot/blob/3.x/scene/main/viewport.cpp#L1861 and thus break dragging modal windows and some yet unclear to me gui focus related code.

jeremyz avatar Oct 18 '21 15:10 jeremyz

@MadDogMayCry0 I can't get your workaround to work with my gui_input(event) example. I suppose it would be possible if I copy it line by line, use _input(event) instead of gui_input(event) and separate scripts like you did, but this is way too complicated for what I want to do.

@MadDogMayCry0 even though it works, what you do with if event.is_pressed(): event.pressed = false is to by pass the code that starts here : https://github.com/godotengine/godot/blob/3.x/scene/main/viewport.cpp#L1861 and thus break dragging modal windows and some yet unclear to me gui focus related code.

Of course, you must adapt my logic to what you are trying to do with your code. My logic only explains and demonstrates how to fix this bug. However, it can be applied to anything, both modals and dialogs and absolutely any situation in general.

MadDogMayCry0 avatar Oct 18 '21 18:10 MadDogMayCry0

@MadDogMayCry0 it seems to me that we don't understand each other. The single line trick you used event.pressed = false, fixes this issue, but creates at least 3 more issues. Add an AcceptDialog in your test project, see that you can't drag the window anymore. Add a TextEdit and see you can't click to focus in it anymore. Add you trick to a drad'n drop test, see it breaks too.

jeremyz avatar Oct 18 '21 21:10 jeremyz

@MadDogMayCry0 it seems to me that we don't understand each other. The single line trick you used event.pressed = false, fixes this issue, but creates at least 3 more issues. Add an AcceptDialog in your test project, see that you can't drag the window anymore. Add a TextEdit and see you can't click to focus in it anymore. Add you trick to a drad'n drop test, see it breaks too.

If you look at the code further than that piece you refer to all the time, you will probably see that there is a fork from the native functions and of course all the approaches you listed like drag and drop and other nonsense you need to add yourself. This works for about 10 minutes of time. I have not argued or asserted that my approach is universal, I will repeat myself once again - this is a demonstration of solving a problem, a demonstration that allows you to bypass native logic and build your own.

MadDogMayCry0 avatar Oct 18 '21 21:10 MadDogMayCry0