hoffstadt/DearPyGui

Tree nodes toggle at mouse button down when clicking on triangle, but with mouse button up when clicking on label.

giogina opened this issue · 5 comments

Version of Dear PyGui

Version: 1.10.1
Operating System: Windows 7

My Issue/Question

The toggle open/close of a tree node is triggered differently depending on whether you click on the triangle or the node label:
When clicking on the triangle, it immediately triggers on mouse button going down (this would be expected behavior); as a result the node's value is true when its item click handler is triggered.
When clicking on the label, the node only changes state after the mouse button is released. This makes any updates that should happen with the toggle wonky, and results in the nodes value being false when its item click handler triggers.

To Reproduce

Run the code below, click on the triangles vs names, see they are wrong when clicking on the labels.
By holding the mouse button down for a moment, you will see that the difference is in when the tree node opens/closes.

Expected behavior

Tree nodes should always toggle on mouse-down.

Standalone, minimal, complete and verifiable example

        import dearpygui.dearpygui as dpg
        
        dpg.create_context()
        dpg.create_viewport()
        dpg.setup_dearpygui()
        
        with dpg.window() as window_id:
        
            dpg.set_primary_window(window_id, True)
        
            def callback(sender, app_data, handler_user_data):
                item = f"node {dpg.get_item_user_data(app_data[1])}"
                state = "open" if dpg.get_value(item) else "closed"
                dpg.configure_item(item, label=f"I am {state}!")
        
            with dpg.item_handler_registry() as node_handlers:
                dpg.add_item_clicked_handler(callback=callback)
        
            for i in range(5):
                with dpg.tree_node(label=f"node {i}", user_data=i, tag=f"node {i}") as n:
                    dpg.bind_item_handler_registry(n, node_handlers)
                    dpg.add_text("Or am I?")
        
        dpg.show_viewport()
        dpg.start_dearpygui()
        dpg.destroy_context()

This is how tree nodes behave in Dear ImGui, which DPG uses as a backend. You can see it if you run dpg.show_imgui_demo() and go into the Widgets section.

Instead of the click handler, try to use the item_toggled_open handler. Unfortunately this one only fires when the node is expanded, and does not when it's collapsed. You can also use an item_visible_handler on the node contents to see if it's expanded or not.

Hm, that's unfortunate.
item_toggled_open only gives half of the information, so it won't do for my purposes. Item_visible triggers on all of the contents of the open child nodes too (which makes the reaction a bit laggy on large trees), and for other reasons than the node being clicked (so I need to keep track of the clicks to persist open-ness anyway).
Well, for now I'm just keeping track of node states manually and hoping the user doesn't minimize the window and trigger Issue #1873. Kind of works, but it would be nice to have a non-hacky option in case there's any way to do that.

Edit: Finally a satisfying-feeling solution (only adding it for whoever runs into the same issue and finds this here on google):
Use item_click_handler to set a flag indicating clicked tree node. Then set a mouse_release_handler to, if the flag is set, trigger the actual code that's supposed to happen on toggle - value, visibility etc are now all consistent with the target state of the toggle process, and buggy node collapses get detected. Phew.

I hope one day to push a PR that makes toggled_open event work on both open and close (as an opt-in behavior, so as not to break existing apps).

Here's another workaround that you might find useful. But it really is a workaround, and it would be better to have DPG behave consistently.

def on_node_click(sender, value, node_id):
    dpg.set_value(sender, False)
    dpg.set_value(node_id, not dpg.get_value(node_id))

@contextmanager
def tree_node_ex(tag: Optional[Union[int, str]] = None, **kwargs) -> Generator[Union[int, str], None, None]:
    with dpg.group(horizontal=True, horizontal_spacing=0):
        node_id = tag or dpg.generate_uuid()
        dpg.add_selectable(callback=on_node_click, user_data=node_id)
        with dpg.tree_node(tag=node_id, **kwargs):
            yield node_id

with dpg.window(label="Tree nodes", width=200, height=150):
    with tree_node_ex(label="Root", default_open=True):
        dpg.add_text("Lorem")
        with tree_node_ex(label="Branch", default_open=True):
            dpg.add_button(label="ipsum")
            with tree_node_ex(label="Leaf", leaf=True):
                pass

It makes nodes toggle on mouse release. Also, you can hook on_node_click and add your own callback (e.g. add it as user_data to the node and then call directly from on_node_click).

It works by placing a selectable beneath the tree node, and capturing mouse clicks with that selectable. No mouse click ever gets through it to the tree node itself. I've wrapped it into a context manager so that it's easier to reuse it in regular code.

Thanks! There are some cool tricks in there that will definitely come in handy.