Autoload UI and how to get data back from it to the original script

Godot Version

4.6.3

Question

Hi! Sorry, I don’t know what keywords to use for this to search for previously answered questions.

I have a scene which I’ve set to autoload. I want another script to call the script attached to that scene. It works alright, but the scene in question is a UI, and it pops under my other UI when I show it by calling a function in the script.

I can get it to pop over all the other UIs by setting Z Index to 1, however, the buttons don’t work. (I’ve currently only hooked up the YES button up via a signal to the script for testing purposes)

(Side note, if you know a better way to divide the screen into a grid than just spamming hbox/vboxcontainers, I’d be happy to hear it lol)

I’d also like to take the result from the dialog box (be it YES or NO) and return that to the script from which we called the function so we can finalize the decision there. I don’t understand how to do that aside from just hooking up the signal from the button directly to a function in the receiving script but that seems like a bad solution.

Here’s the code for where I’m calling it from (the inventory page in my pause menu):

extends Control
@onready var item_list: VBoxContainer = $"Split/Left column/ScrollContainer/Item list"
@onready var mainpausemenu: Control = $"../mainpausemenu"
@onready var item_name: Label = $"Split/Right column/VSplitContainer/item name container/item name"
@onready var item_quantity: Label = $"Split/Right column/VSplitContainer/item quantity container/item quantity"
@onready var item_description: Label = $"Split/Right column/VSplitContainer/PanelContainer/item description"
@onready var sub_viewport: SubViewport = $"Split/Right column/VSplitContainer/SubViewportContainer/SubViewport"
@onready var sub_viewport_container: SubViewportContainer = $"Split/Right column/VSplitContainer/SubViewportContainer"

func _ready() -> void:
	inventoryhandler.inventory_updated.connect(update_inventory)

func update_inventory():
	# clear the list before updating it
	for i in item_list.get_children():
		item_list.remove_child(i)
		i.queue_free()
	# update the list
	for i in inventoryhandler.inventory:
		if i != null:
			
			var instance = load(i["item_scene"]).instantiate()
			
			# for every item in our inventory, make a button
			var button = MenuButton.new()
			button.text = instance.item_name
			
			# then we add "use" and "drop" to the button's drop down menu
			button.get_popup().add_item("Use")
			button.get_popup().add_item("Destroy")
			button.get_popup().id_pressed.connect(dropdown.bind(i["item_scene"]))
			button.flat = false # (this is just for looks, personal preference)
			
			# then we send all the data from the item to a function
			# so when we mouse over the item (or enter focus with a gamepad)
			# it'll update stuff in the right column with the data we provided here
			button.mouse_entered.connect(right_column_info.bind(instance.item_description).bind(i["quantity"]).bind(instance.item_name).bind(i["item_scene"]))
			button.focus_entered.connect(right_column_info.bind(instance.item_description).bind(i["quantity"]).bind(instance.item_name).bind(i["item_scene"]))
			
			# this time we clear all the info whenever we're not mousing over anything
			# (or moved focus to a different element with gamepad)
			button.mouse_exited.connect(clearinfo)
			button.focus_exited.connect(clearinfo)
			
			# then we make the button actually visible on screen by
			# assigning it as a child of the item list container
			item_list.add_child(button)

# handle the "back" button
func inventory_back() -> void:
	mainpausemenu.show()
	hide()

# this is where we handle the data that is sent over by the buttons
func right_column_info(itemscene,itemname,itemquantity,itemdescription):
	var instance = load(itemscene).instantiate()
	sub_viewport.add_child(instance)
	item_name.text = itemname
	item_quantity.text = str(itemquantity)
	item_description.text = itemdescription

# clear the info whenever we're not hovering over anything
func clearinfo():
	var instance = sub_viewport.get_child(0)
	sub_viewport.remove_child(instance)
	item_name.text = ""
	item_quantity.text = ""
	item_description.text = ""

# 0 is the first item in the menu, so "use"
# 1 is the second, which is "destroy"
func dropdown(id,scene):
	if id == 0:
		var item = load(scene).instantiate()
		item.use()
		inventoryhandler.remove_item(scene,1)
	if id == 1:
		destroyitemconfirm.confirmation()
		# remove the item if user answered YES, otherwise bail out
		#inventoryhandler.remove_item(scene,1)

Here’s the code for the confirmation dialog:

extends Control

func confirmation():
	show()

func yes_pressed() -> void:
	print("yes")

Yeah, I tried that, but it doesn’t really divide it into grids. You still need a bunch of elements to set the rows since you can only set the number of columns

Yup. Still works. I use it one of my projects to precisely place controls on a map.

class_name ScreenGridColumn
extends VBoxContainer

var min_row: int = 1
var max_row: int = 10


func get_row(num: int) -> MarginContainer:
	
	if num < min_row: num = min_row
	elif num > max_row: num = max_row
	
	return get_node(str("MarginContainer",num))
	

In the debugger panel the “Misc” tab can show you which UI element is being clicked on. Use that to determine which node should be ignore mouse input. This “grid” layout certainly is strange, seems like you could achieve the effect with one panel container → Vbox → Label, Hbox → button, button

The clicks are just going straight through, as if the menu never existed. I think the Z Index is just changing the visibility instead of actually bringing it forwards?

Right, so I didn’t know how to search for this problem online, but I’ve since wisened up.

The autoloaded scenes ALWAYS get loaded before the scene itself, meaning it was ALWAYS going to be under everything. The z-index was indeed just visually bringing it forward, it wasn’t bringing the actual layers forward so that a mouse could interact with it.

The solution was to change the autoloaded scene to be a CanvasLayer and attaching the script to that instead of the Control node so it’d hide/show the canvaslayer whenever needed. Since I wasn’t using any other canvaslayers in my project before, keeping the layer set to 1 was enough to make it always draw over everything else.

Waiting for user input on the buttons was easy enough, here’s the inventory code which calls for the confirmation dialog:

# 0 is the first item in the menu, so "use"
# 1 is the second, which is "destroy"
func dropdown(id,scene):
	if id == 0:
		# If "use" was pressed, instantiate the item (to load its script) and call its use() function
		# Then remove one of them from our inventory
		var item = load(scene).instantiate()
		item.use()
		inventoryhandler.remove_item(scene,1)
	if id == 1:
		# If we're choosing to destroy, await for user confirmation. If yes, remove item. If no, pass.
		var confirmation = await destroyitemconfirm.confirmation()
		if confirmation:
			inventoryhandler.remove_item(scene,1)
		else:
			pass

Here’s the code for the confirmation dialog box:

extends CanvasLayer

signal choice

# Pop up a dialog box asking if we really want to destroy an item
# then wait for the user to make a choice
func confirmation():
	show()
	var chosen = await choice
	if chosen:
		hide()
		return true
	else:
		hide()
		return false

func yes_pressed():
	choice.emit(true)

func no_pressed():
	choice.emit(false)

Idk if it’s the best way of handling things but it works so whatever