DEV Community

Kyle Homen
Kyle Homen

Posted on

Release 0.4 Results

We're at the end of the road now. Today I submitted the pull request to display top categories as individual tabs to the Pandora project.

This ended up being a fun little challenge and I'm really happy with how it turned out.

What I Did

The goal was to add a setting to turn this default, tree-style look:

Into one with tabs. The logic for creating new categories and entities already existed, so this was mostly UI.

Building the Tab system

The core logic lives in the PandaEditor scene. Within the Godot editor itself, you can add nodes to each scene, including containers and UI elements.

I started by adding a TabContainer to the scene and setting it to hidden. This will eventually be populated with tabs when the setting is chosen and become visible.

pandora-settings holds the code for adding settings, so there I added a boolean toggle to swap to tab view:

const USE_CATEGORY_TABS = "pandora/use_category_tabs"

static func get_use_category_tabs() -> bool:
    return ProjectSettings.get_setting(USE_CATEGORY_TABS, false)
Enter fullscreen mode Exit fullscreen mode

Next, I added tab populating to pandora-editor.gd. There already existed a function called _populate_data() that handled data by default, but I needed to modify it to check if the tabs setting was set:

    var use_tabs = PandoraSettings.get_use_category_tabs()

    if use_tabs:
        await _populate_tabs(data)
    else:
        tree.set_data(data)
Enter fullscreen mode Exit fullscreen mode

Then I created the _populate_tabs() function to take the data, and instead of mapping it to a tree, create a tab for each root category:

# Create tabs for root categories
    for i in range(root_categories.size()):
        var tab_content = Control.new()
        category_tab_container.add_child(tab_content)
        category_tab_container.set_tab_title(i, root_categories[i].get_entity_name())
Enter fullscreen mode Exit fullscreen mode

Now I needed to filter the tree to show only the children of the selected category when the tab was selected, so I created the _on_tab_changed() function:

func _populate_tabs(root_categories: Array[PandoraEntity]) -> void:
    # ... create tabs ...

    # Store root categories
    category_tab_container.set_meta("root_categories", root_categories)

    # Select tab
    category_tab_container.tab_changed.connect(_on_tab_changed)

func _on_tab_changed(tab_index: int) -> void:
    var root_categories = category_tab_container.get_meta("root_categories", []) as Array
    var selected_category = root_categories[tab_index]

    # Filter tree to show only entities in the selected category
    var filtered_data:  Array[PandoraEntity] = []
    filtered_data.assign(selected_category._children)
    tree.set_data(filtered_data)
Enter fullscreen mode Exit fullscreen mode

Now clicking on the tabs filtered the tree to only show the root categories' child nodes. From here, functionality worked like the original tree view and you could add and remove entities from each tab, but it was missing something crucial: The tab categories could not be removed or renamed, and we needed a way to create categories with a '+' button.

I started by adding the '+' tab to _populate_tabs(), created after the root categories:

func _populate_tabs(root_categories:  Array[PandoraEntity]) -> void:
    # ... create category tabs ...

    # Add '+' tab for creating new root categories
    var add_tab_content = Control.new()
    add_tab_content.name = "+"
    category_tab_container.add_child(add_tab_content)
Enter fullscreen mode Exit fullscreen mode

then updated _on_tab_changed() to detect when the '+' tab was clicked:

func _on_tab_changed(tab_index:  int) -> void:
    var root_categories = category_tab_container.get_meta("root_categories", []) as Array

    # Create new root category if '+' tab is selected
    if tab_index >= root_categories.size():
        _create_root_category()
        return
Enter fullscreen mode Exit fullscreen mode

To rename and delete categories, I determined that adding a context menu was the best option, and mapped it to the right mouse button:

func _on_tab_bar_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
        var tab_bar = category_tab_container.get_tab_bar()
        var clicked_tab = tab_bar.get_tab_idx_at_point(event.position)

        if clicked_tab >= 0:
            var root_categories = category_tab_container.get_meta("root_categories", []) as Array

            # Don't show menu for '+' tab
            if clicked_tab >= root_categories.size():
                return

            # Store which tab was clicked
            category_tab_container.set_meta("context_menu_tab", clicked_tab)

            # Show context menu
            tab_context_menu.position = tab_bar.get_screen_position() + event.position
            tab_context_menu.popup()


func _on_tab_context_menu_pressed(id: int) -> void:
    var clicked_tab = category_tab_container.get_meta("context_menu_tab", -1)
    if clicked_tab < 0:
        return

    var root_categories = category_tab_container.get_meta("root_categories", []) as Array
    if clicked_tab >= root_categories.size():
        return

    var category = root_categories[clicked_tab]

    match id:
        0:  # Rename
            _show_rename_dialog(category, clicked_tab)
        1:  # Delete
            _show_delete_confirmation(category, clicked_tab)
Enter fullscreen mode Exit fullscreen mode

I also went back and hid the original create category button and needed to make the create entity button enabled when in tab view. Oh, and I need to make the category properties appear when the tab was selected, and probably a few other things I forgot. But here is the finished view:

and the setting toggle in project settings:

From here there was one last issue I found, and that was that the setting didn't automatically take and I needed to reload the project each time. I found a simple way to do that by adding a settings listener in the ready() function and re-populating the data (this will see that the category tab setting is true and switch to tab view):

func _ready() -> void:
    ProjectSettings.settings_changed.connect(_on_settings_changed)

func _on_settings_changed() -> void:
    _populate_data()
Enter fullscreen mode Exit fullscreen mode

... and that's about it I think. I managed to learn more about GDScript and Godot engine through adding this feature. Everything is a node! I learned to work with signals, and manage different UI states. I think it turned out great, and I got some positive feedback right away from the maintainers and only need to make some minor changes to rename a node and add % notation.

Top comments (0)