+2 votes

I'm trying to make an ItemList that wraps around: if you're at the top and you press Up, you'll go to the bottom. But try as I might I can't figure out how to handle InputEvents in my script such that the ItemList code doesn't also handle them.

Here's an example ItemList script that demonstrates my issue:

extends ItemList

func _ready():
    add_item("One")
    add_item("Two")
    add_item("Three")
    add_item("Four")
    add_item("Five")
    set_process_input(true)

func _input(event):
    if event.type == InputEvent.KEY and event.is_pressed():
        print("_input: ", event)
        get_tree().set_input_as_handled()

func _input_event(event):
    if event.type == InputEvent.KEY and event.is_pressed():
        print("_input_event: ", event)
        accept_event()

The official documentation states

First of all, the standard input function will be called [...] If any function consumes the event, it can call SceneTree.setinputashandled(), and the event will not spread any more.
[...]
Second, it will try to feed the input to the GUI, and see if any control can receive it. If so, the Control will be called via the virtual function Control.inputevent() and the signal “inputevent” will be emitted [...] If the control wants to “consume” the event, it will call Control.acceptevent() and the event will not spread any more.

In input(event) I call gettree().setinputashandled(), which should prevent the event from propagating. But it still gets sent to _inputevent(event). From there I call accept_event(), which should also prevent the event from propagating, but the ItemList code will still happily act on it.

Try creating an ItemList with this code, clicking within it, and pressing Up or Down (as appropriate). You'll see two printouts for each InputEvent and the ItemList selection will move accordingly, even though the documentation suggests that this should not happen.

Clearly I'm misunderstanding something. How do I go about preventing an InputEvent from propagating?

Update

I haven't figured out how to stop an InputEvent from propagating, but I did figure out how to make a circular ItemList, which is good enough for this discussion. (Thanks, avencherus).

Assign the script below to an ItemList and you'll be able to navigate from the top to the bottom or the bottom to the top:

extends ItemList

func _ready():
    set_process_input(true)

# Wrap around from the top to the bottom and vice versa.
func _input(event):
    if not has_focus():
        return
    var first = 0
    var last = get_item_count() - 1
    if (event.type == InputEvent.KEY or event.type == InputEvent.JOYSTICK_BUTTON) and event.is_pressed() and not event.is_echo():
        if event.is_action("ui_up"):
            if is_selected(first):
                call_deferred("_self_select", last)
        if event.is_action("ui_down"):
            if is_selected(last):
                call_deferred("_self_select", first)

func _self_select(index):
    get_tree().set_input_as_handled()
    select(index)
    emit_signal("item_selected", index)

This appears to work correctly because of call_deferred: with immediate calls, this code and ItemList's code will step on each others' toes when moving into the top or bottom slot. Try it for yourself if you're curious.

in Engine by (103 points)
edited by

2 Answers

+2 votes
Best answer

As far as the issues with dealing with the input on controls, you'd have to use things like checking to see if the control has focus or not, when dealing with these events.

I'm not sure if it's a bug. Maybe not so well designed in some respects.

So for what you're wanting you could potentially do something like this:

extends ItemList

func _ready():

    add_item("One")
    add_item("Two")
    add_item("Three")
    add_item("Four")
    add_item("Five")

    set_process_input(true)

# Used as a deferred call, so the spamminess of input doesn't cause issues.
func set_item(index):
    select(index)

func _input(event):

    # Indexes (For readability)
    var first = 0
    var last = get_item_count() - 1

    if(has_focus()):

        if(event.is_action_pressed("ui_up") and is_selected(first)):
            call_deferred("set_item", last)

        elif(event.is_action_pressed("ui_down") and is_selected(last)):
            call_deferred("set_item", first)

Then also, the control has it's own signal for handling input. So it behaves more like what you had wanted. So you won't need to check for focus, but it is a little odd in it's timing for this kind of thing you're doing, so you will still need to do some special checking of things. If you want to use the signals, you could write something like this instead:

extends ItemList

var current_selection = -1
var previous_selection = -1

func _ready():

    add_item("One")
    add_item("Two")
    add_item("Three")
    add_item("Four")
    add_item("Five")

    connect("input_event", self, "input_received")
    connect("item_selected", self, "selection_made")

# There's not a built-in method for just getting an selected index for single selections
func get_selected():

    for item in get_selected_items():
        return item

    return -1

func input_received(ev):

    # Indexes (Again for readability sake)
    var first = 0
    var last = get_item_count() - 1

    if(previous_selection == current_selection):

        if(ev.is_action_pressed("ui_up") and current_selection == first):
            select(last)

        elif(ev.is_action_pressed("ui_down") and current_selection == last):
            select(first)

    previous_selection = current_selection  

func selection_made( index ):

    previous_selection = current_selection
    current_selection = index
by (5,203 points)
selected by

This solves my specific problem, though I'm still not convinced that there isn't a bug with regard to handling input, but I'll make a separate issue for that when I've come up with an even more minimal example. Thanks.

You're welcome. X)

Maybe so, hard to know what the intention was, but it's worth checking.

0 votes

If your goal it to stop input from reaching the ItemList, I believe you have to handle this at least a node above if you want to consume the event and not have it propogate further.

At the point you're calling it, it's too late, the event is being handled. All it would do there is have the ItemList handle the event and report it has handled it. So it will not go further from there.

by (5,203 points)

I restructured my above example so there are two scenes. One is an ItemList with the following code:

extends ItemList

func _ready():
    add_item("One")
    add_item("Two")
    add_item("Three")
    add_item("Four")
    add_item("Five")

func _input_event(event):
    if event.type == InputEvent.KEY and event.is_pressed():
        print("_input_event: ", event)
        accept_event()

The other is a Node, whose only child is an instance of the above ItemList. The Node has the following code:

extends Node

func _ready():
    set_process_input(true)

func _input(event):
    if event.type == InputEvent.KEY and event.is_pressed():
        print("_input: ", event)
        get_tree().set_input_as_handled()

The parent (Node) catches the InputEvent first, in input(event), just like the docs say. That event should be consumed by the call to gettree().setinputashandled(), but it is still caught by the child (ItemList)'s _inputevent(event) and the actual ItemList itself.

So it does not appear that handling it a node above does the trick.

Sorry it seems I'm unclear about your goal.

If you want to stop input to controls, that is effectively a disabled control. Some offer that option on the control or the items inside.

But otherwise I don't think Godot supports the ability to render GUI items completely unresponsive to input.

In your first post you talked about how you wanted to alter the order of how things worked. I would think you would want to handle the input and make logic decisions based on the input to accomplish what you want.

I'd like to have the ItemList wrap around -- if you press Up while you're at the top, it goes to the bottom, and vice versa. So logically, I check to see if "ui_up" was just pressed and if the ItemList has item 0 selected.

The problem is, this can happen at two times: either the user is at the top of the list, in which case I do want them to wrap, or the user just pressed up and the ItemList code just moved them to the top of the list, in which case the above logic will cause them to skip from the second item to the bottom of the list. But I can't figure out how to tell when the ItemList code has acted upon an InputEvent, so it gets acted on twice.

But regardless of what I want to accomplish, in the above code I have a parent node setting an InputEvent as handled earlier in the input-processing hierarchy but a later node still receives that InputEvent. Neither you, I, nor the official documentation state that should happen, so either it's a bug or I'm doing something wrong.

Well since I think I clear view of the goal, I will post another answer, so I hope it's what you're after.

Thanks; the primary thing I'm looking for is how to consume an InputEvent such that it does not get passed along. The scrolling list just happens to be an example of that use case, but I can come up with others where calling setinputas_handled() does not appear to stop the input from spreading.

Ok, well hopefully my solutions there are what you're looking for. I believe you can use the input event signals attached to the control's themselves to handle the input in a more desirable way.

I don't think the GUI is designed to ignore the input from such things, because it doesn't make a lot of sense to make active GUI elements that are non-responsive to input.

That's what I think the bug might be -- the documentation clearly claims that setinputashandled can be called to prevent something from showing up to the GUI, and makes a note that you should have game logic check unhandledinputs, which happens after the GUI receives and maybe handles the events, that way your game logic doesn't prevent input from reaching the GUI.

But again, I'll create a more general topic for that after I've made a better example. Possibly on the forums. Thanks for the attention.

True. I would think it would be more consistent if it worked as it's stated, but then I wonder about the moment you begin to absorb input events, all controls would suddenly stop getting input. Not just the one you'd prefer.

I imagine a scenario where you just wanted some node to not pass along a click to another node, and then suddenly all your GUI items became deaf to it. Wouldn't that be a potential hassle?

If you blacklisted the UP event for even items not related to GUI, you could have a situation where you have several lists, and now they can all scroll down by pressing down, but never up.

It's just my impression they separated that for certain reasons, and it's possible the documentation was not accurately written, or out of date.

That's just my guess.

I have a hard time imagining where to draw the line between handling input for manipulating the control, and generating responses. They're sort of married.

For this kind of thing, they'd probably be better off just exposing more methods that allow you to override the control's behavior.

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.