inputfocusgodotgdscriptgodot4

Handle Control losing focus between button press and release events


I'm making a small game with Godot, and the player can keep some panels on top of the game display while playing, for easy access to some actions.

The panel is a Control node, and it could appear on top of the main game display, which is of type Node2D. Both the panel and the Node2D nodes have their own handling for mouse clicks, but if I click on the panel, I expect only the panel will get the button press and release events, not the Node2D underneath it.

I made a simple reproducible example.

This is the scene tree and the corresponding 2D view Scene tree and 2D view

The input handling is in the Sprite2D and PanelContainer scripts seen in the picture.

The script of the sprite/node2D:

extends Sprite2D

func _unhandled_input(event):
    if event is InputEventMouseButton:
        print("Mouse pressed=%s event in 2D nodes" % event.pressed)
        get_viewport().set_input_as_handled()

The script for the panel:

extends PanelContainer

func _gui_input(event):
    if event is InputEventMouseButton:
        print("Mouse pressed=%s event in control nodes" % event.pressed)
        get_viewport().set_input_as_handled()

According to the Godot documentation, GUI input events (handled by the method _gui_control) will be processed before unhandled input (by the method _unhandled_input). And that is indeed the case during normal execution. However, if I am debugging and placing a breakpoint on the print message of _gui_input in the panel script, the following happens:

  1. 'Mouse button press' event emitted
  2. 'Mouse button press' event enters _gui_input and gets to the print statement
  3. Debugger stops at the breakpoint and focus changes from the game to the Godot editor
  4. Continue execution
  5. The panel has now lost focus, so the 'mouse button release' event is not caught by _gui_input.
  6. The 'mouse button release' event is caught by the _unhandled_input method

Something like this can be reproduced without a debugger as well.

  1. Click and hold the button pressed on the panel
  2. Alt+Tab to switch to another window without moving the mouse
  3. Release the mouse button

The behavior in step 6 is not wanted. If the player clicks and releases the click on the panel, I would like to distinguish between the event being on top of a UI element, or it being outside of the UI element (independent of whether it's on the sprite or not). But the release event is sent to _unhandled_input, which is processed by the game display in the background of the panel. And the player may not see the result of the click behind the panel, leading to a bad player experience.

I cannot make the panel modal (in the sense that while the panel is visible, nothing else can handle mouse clicks) because the player should be able to click on the area outside the panel and have that clicked be handled normally.

I cannot use the _input method to handle the event because that runs before the _gui_input, and it doesn't let the Control nodes handle GUI input.

Is my only choice adding a check myself in _unhandled_input or _input to see if the position of the 'mouse button release' event is within the bounds of the panel/UI? It doesn't seem like the correct way to do things with Godot, since the _gui_input method is the one specifically designed to handle GUI input.


Solution

  • Godot can listen to OS notifications about the window losing focus. See doc for sample notifications it can listen to. The _unhandled_input call can ignore the event if the game window is not focused, so the button release event can be handled differently in that special case.

    For example, add a singleton script that listens to the focus in/out notifications:

    class_name FocusChecker
    extends Node
    
    static var focused: bool = true
    
    func _notification(notification):
        match notification:
            NOTIFICATION_APPLICATION_FOCUS_IN:
                focused = true
            NOTIFICATION_APPLICATION_FOCUS_OUT:
                focused = false
    

    And you can check the status in _unhandled_input to decide how you want to process the event.

    extends Sprite2D
    
    func _unhandled_input(event):
        if event is InputEventMouseButton:
            if (FocusChecker.focused):
                print("Normal processing of unhandled input")
            else:
                print("Special case handling of unhandled input")
            get_viewport().set_input_as_handled()
    

    You could do a similar thing for only the control losing focus, and not the whole window. You can listen to the focus_exited signal of the control, and also keep track of whether the last mouse button event was a press or release. If the focus_exited event happens between a press and a release, set a global flag that can be accessed by input handling methods.