How to dynamically show search results of LineEdit

Godot Version

Godot 4.2

Question

Im making an address input field. I have a LineEdit set up to start typing in the address. It searches for results over google maps api, as you type.

I’d like to show the suggested results below the LineEdit so it can be picked from.

I’ve tried adding a PopupMenu, setting its position, adding items.
The PopupMenu shows up with correct results, but when it does, it grabs the focus from the LineEdit and I can no longer keep typing unless I click again on the LineEdit.

How can I get the focus of the LineEdit again and keep the results displayed?
Then if needed, clicking on them or going through them with UP/DOWN keys?

You probably want to set the Window.unfocusable property on your popup when creating it. Setting this to true will fix your focus problem, but you’ll probably need to manually handle certain inputs.

You can also use Control.grab_focus to set the focus to a specific control.

One thing you can do is something like this:

popup.unfocusable = true
popup.show()
popup.unfocusable = false

Unfortunately its still the same.
I’ve tried grabbing focus with the LineEdit, but it seems like it still has it. It has the same outline.

I’ve also added signals to the LineEdit to see when it enter/exits focus and when the popup shows up. The focus exit signal is not called.

I guess that’s why the grab focus doesnt work.

The popup just steals the keyboard inputs

I tried with a PopupMenu and it kinda work as long as embedded subwindows in the project settings is enabled (it’s enabled by default) but you have to do a couple of workarounds to avoid it stealing the focus while still being able to use the mouse to select stuff.

script.gd
extends LineEdit


@onready var popup_menu: PopupMenu = $PopupMenu


var selected_idx := -1
var filtered_list = []


func _ready() -> void:
	# Grab focus when ready
	grab_focus()
	# When the text_changed signal is fired, filter the list
	text_changed.connect(_filter_list_and_popup)
	# start with the popup not being able to grab focus
	popup_menu.unfocusable = true

	# When the popup menu is about to popup make it focusable so we can control it with the mouse
	# connect it in a deferred way so it does it at the end of the frame and does not steal the focus
	popup_menu.about_to_popup.connect(func():
		popup_menu.unfocusable = false
	, CONNECT_DEFERRED)
	# When the popup menu is going to hide make it unfocusable again
	popup_menu.popup_hide.connect(func():
		popup_menu.unfocusable = true
	)

	# Connect the index_pressed to update_text
	popup_menu.index_pressed.connect(update_text)


func _gui_input(event: InputEvent) -> void:
	# If popup isn't visible, keep the normal behavior
	if not popup_menu.visible:
		return

	# if it's visible we check if the action is ui_up or ui_down
	# and act accordingly

	if event.is_action_pressed("ui_up", true):
		# wrap the selected_idx up, focus the item in the popup and accept the event
		selected_idx = wrapi(selected_idx - 1, 0, filtered_list.size())
		popup_menu.set_focused_item(selected_idx)
		accept_event()
	if event.is_action_pressed("ui_down", true):
		# wrap the selected_idx down, focus the item in the popup and accept the event
		selected_idx = wrapi(selected_idx + 1, 0, filtered_list.size())
		popup_menu.set_focused_item(selected_idx)
		accept_event()
	if event.is_action_pressed("ui_accept"):
		# update the text to the selected index, hide the popup and accept the event
		update_text(selected_idx)
		popup_menu.hide()
		accept_event()


func update_text(index:int) -> void:
	# set the text to the filtered list value and se the caret column to the end of the text
	text = filtered_list[index]
	caret_column = text.length()


func _filter_list_and_popup(input_text:String) -> void:
	if input_text.length() < 2:
		# if input_text is less than 2 characters hide the popup and set the selected index to -1
		popup_menu.hide()
		selected_idx = -1
		return

	# generate the filtered list by sorting the FRUITS list checking their similarity with the input_text
	filtered_list = FRUITS.duplicate()
	filtered_list.sort_custom(func(a:String, b:String): return a.similarity(input_text) > b.similarity(input_text))

	# Resize it to the first 10 values
	filtered_list.resize(10)

	# clear the popup list and fill it
	popup_menu.clear()
	for value in filtered_list:
			popup_menu.add_item(value)

	if not popup_menu.visible:
		# if the popup is not visible, make it popup at the bottom of the line edit
		popup_menu.popup(Rect2(position + Vector2(0, size.y + 2), size))

	# select the first value and focus it
	selected_idx = 0
	popup_menu.set_focused_item(selected_idx)


const FRUITS = [
 "apple", "apricot", "avocado", "banana", "bell pepper", "bilberry", "blackberry", "blackcurrant", "blood orange", "blueberry", "boysenberry", "breadfruit", "canary melon", "cantaloupe", "cherimoya", "cherry",
 "chili pepper", "clementine", "cloudberry", "coconut", "cranberry", "cucumber", "currant", "damson", "date", "dragonfruit", "durian", "eggplant", "elderberry", "feijoa", "fig", "goji berry", "gooseberry", "grape", "grapefruit", "guava", "honeydew", "huckleberry", "jackfruit", "jambul", "jujube", "kiwi fruit", "kumquat", "lemon", "lime", "loquat", "lychee", "mandarine", "mango", "mulberry", "nectarine", "nut", "olive", "orange",
 "papaya", "passionfruit", "peach", "pear", "persimmon", "physalis", "pineapple", "plum", "pomegranate", "pomelo", "purple mangosteen", "quince", "raisin", "rambutan", "raspberry",
 "redcurrant", "rock melon", "salal berry", "satsuma", "star fruit", "strawberry", "tamarillo", "tangerine", "tomato", "ugli fruit", "watermelon"
]

Result:

It may be easier to create a fully custom control that shows the list of values and let you select one though.

Thank you! I was able to get it working using your code :slight_smile:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.