Focus Neighbor on identical nodes in specific group?

Godot Version

4.7

Question

Is there a way to have Focus Neighbor only work on a set group of control nodes?

I’d like for keyboard/controller controls to move through a list of buttons that are generated in runtime (buttons). And to do so without being able to jump back to other buttons, elsewhere in the menu. At the same time, the mouse should still be able to focus and click all buttons, so I can’t turn their focus off entirely.

Each button is made from the same, single scene that holds both button, label and icon, and added as children to a grid container.

Since all these buttons are made from the same scene, I’m not sure how I’d be able to set this through the editor, so I imagine it would have to be through code, but I’m not sure how. I can already focus on them fine when clicking a category, but I can’t decide neighbors.

Visualised example - after picking a category, I want the keyboard/controller to navigate only the added buttons in green, not the buttons in red:

Assign the neighbors after the runtime generation.

How would I do that? And would there be a way to always make it work even though you can’t know ahead of time how many buttons spawn, and what their names are?

Well if you’re spawning them via code then your code must know their positions. Use that information to figure out each button’s neighbors.

I’m afraid I don’t really know how to do that, hence my question.

How do you set the positions?

They are all just listed in a grid container.

Where’s the problem then? The grid dimensions implicitly dictate who’s whose neighbor. You presumably generate them in a loop, so you know an index of every button and thus the indexes of its neighbors.

I guess the problem is that I don’t know how to actually do that in code.

Post your spawning loop.

Alrighty, here it is - item_columns is the grid container they are being added to:

Get the array of items by calling get_children(), iterate through them, count the index and assign the neighbors. The left/right neighbors will have the index that’s +/- 1 of the current index and up/down neighbor indexes will be +/- the number of columns in the grid. Check and skip the boundary cases when there is no particular neighbor - first/last row/column.

Btw, when posting code, always post it in text format, not as a screenshot.

Sorry about the late response - will try if I can get something like that set up and going! Thanks!

And I will keep that in mind!

So you would setup each neighbor in each group and then you would also have to make your code remember the last focus of each group and have a button or action (like pressing the tab key for example) that jumps back to the last item which had focus in each group.

An even more advanced way would be to have second set of relations for each button: the first within the group defining each neighbor and the second set would express which nearest button in the other group to jump to.

As far as I know there is no such stuff pre-built into Godot. But certainly one can built such stuff by oneself. Doing it manually is a bit hard tho, so it would be really good to know if there is a way to return a nearest neighbor of a control element for a set of control elements of interest.

Please post an update, if you find out more about it. I would be eager to know!

I experimented with trying this, and probably didn’t do it right at all - this is the result so far, for making an array and going through it:

var button = get_node("%ItemColumns/InventorySlot/Button")
	button.grab_focus()
	
	var all_slots : Array
	for slot in item_columns.get_children():
		all_slots.append(slot)

	var slot_index = 0
	for slot in all_slots:
		var prev_slot = slot_index
		var next_slot = slot_index + 2
		button.focus_neighbor_top = all_slots[prev_slot].button
		button.focus_neighbor_left = all_slots[prev_slot].button
		button.focus_neighbor_bottom = all_slots[next_slot].button
		button.focus_neighbor_right = all_slots[next_slot].button
		slot_index += 1

It gets me this error when trying to run it:

“Invalid assignment of property or key ‘focus_neighbor_top’ with value of type ‘Button’ on a base object of type ‘Button’.”

I think it’s something to do with how the focus_neighbour command wants a path, like I did originally when making the button var? But unsure how to translate that into the focus-setting code.

focus takes node paths, not nodes. you could use .get_path() on each of your buttons when assigning focus

My experiment now has this maybe ugly result - the loop seems to go through each index fine as it is told to do, sorting which ones are the first and last as well, but only the slot for the first item has its neighbors correctly set when I test it. The others seem to have had no change.

What could I be doing wrong?:

var all_slots : Array # Create array of all currently shown slots in order
	all_slots.clear() # clear it first, just in case.
	for slot in item_columns.get_children():
		all_slots.append(slot)
	var slot_index : int = 0 # What slot are we currently setting neighbours on
	var index_size : int = all_slots.size() # Get array size for final index number. Always 1 higher than the index.
	print(index_size)
	print(all_slots)
	
	for slot in all_slots:
		if (all_slots.is_empty()): # Check if there are items
			pass
		else:
			var prev_slot = slot_index
			
			if index_size == 1: # When there is only one item
				button.focus_neighbor_top = all_slots[-1].button.get_path()
				button.focus_neighbor_left = all_slots[-1].button.get_path()
				button.focus_neighbor_bottom = all_slots[0].button.get_path()
				button.focus_neighbor_right = all_slots[0].button.get_path()
				print("set first singular item, slot index: ", slot_index)
				
			else: # More than one item
				if slot_index == 0: #First item
					button.focus_neighbor_top = all_slots[-1].button.get_path()
					button.focus_neighbor_left = all_slots[-1].button.get_path()
					button.focus_neighbor_bottom = all_slots[1].button.get_path()
					button.focus_neighbor_right = all_slots[1].button.get_path()
					print("set first item, slot index: ", slot_index)
					slot_index += 1
				else:
					if slot_index <= (index_size - 2): # All items inbetween the first and last
						var next_slot = slot_index + 1
						prev_slot = slot_index - 1
						button.focus_neighbor_top = all_slots[prev_slot].button.get_path()
						button.focus_neighbor_left = all_slots[prev_slot].button.get_path()
						button.focus_neighbor_bottom = all_slots[next_slot].button.get_path()
						button.focus_neighbor_right = all_slots[next_slot].button.get_path()
						print("set middle items, slot index: ", slot_index)
						slot_index += 1
						
				if slot_index == index_size -1: # The final item
					prev_slot = slot_index - 1
					button.focus_neighbor_top = all_slots[prev_slot].button.get_path()
					button.focus_neighbor_left = all_slots[prev_slot].button.get_path()
					button.focus_neighbor_bottom = all_slots[0].button.get_path()
					button.focus_neighbor_right = all_slots[0].button.get_path()
					print("set last item, slot index: ", slot_index)
					slot_index = 0

If you only want to prevent the focus from leaving the grid you may only need to set the focus for the border items, the first row and left-most column.

How would I change that code to do that?

The slots are all added one-by-one, and split between the two columns automatically as space permits, so I don’t necessarily know ahead of time which one goes where.

A grid container will fill one row from left-to-right before moving on to the next row, the top row is your first 0..columns children, the left-most column is every modulus columns child

for i in $GridContainer.get_child_count():
    var is_top: bool  = i < $GridContainer.columns
    var is_left: bool = (i % $GridContainer.columns) == 0
    var child: Control = $GridContainer.get_child(i)