hoffstadt / DearPyGui

Dear PyGui: A fast and powerful Graphical User Interface Toolkit for Python with minimal dependencies
https://dearpygui.readthedocs.io/en
MIT License
12.96k stars 677 forks source link

Be able to get mouse position within Node Editor #2164

Open julioasotodv opened 1 year ago

julioasotodv commented 1 year ago

Is your feature request related to a problem? Please describe.

I am trying to make a Drag and Drop node editor, where I have a sidebar from which I can select nodes to add to a Node Editor area:

sc1

I have implemented already the drag and drop functionality. However, dragged nodes appear in a wrong position (and not where the drop callback happens).

This is due to how the drop callback works: I just get the mouse position using dpg.get_mouse_pos(local=False), and create a new Node in the Node Editor using that position.

The issue here is that positioning within the Node Editor work differently (it has its own coordinate system, which allows for panning through the editor by clicking the mouse wheel).

You can see the issue here:

drag_demo

Describe the solution you'd like

A simple way to get the mouse position within the Node Editor would solve the issue (using the same positioning system that dpg.get_item_pos(some_node_item)).

Describe alternatives you've considered

The only alternative I can think of is to always create new nodes in position [0,0], but then drag and drop would not make sense.

Additional context Nothing noteworthy

Thank you! I really like the library.

cloasdata commented 1 year ago

You could query the items left and top of the editor for their rect width and rect height. With that you could then calculate the relative mouse position.

julioasotodv commented 1 year ago

@cloasdata thank you for the response, but I believe that this way I would still have no info on where the mouse is within the Editor coordinate system (which works independently of the usual one that is used in the rest of DearPyGui).

cloasdata commented 1 year ago

For me this works. The solution with the reference node does not work, as dpg dies when try to hide the node.

PhilippThoelke commented 11 months ago

The solution with the reference node works for me by simply initializing it far away where it is unlikely to be found, since hiding causes a Segmentation Fault (#2151).

Then I can calculate the mouse position in node editor coordinate space using the mouse position from dpg.get_mouse_pos(local=False), the initial reference node position that you set, and dpg.get_item_state(ref_node)["rect_min"].

Clearly less than ideal but it works.

julioasotodv commented 11 months ago

What do you mean with "the reference node"? Is there any other related issue that already presents a solution for this?

PhilippThoelke commented 11 months ago

The reference node is just an empty node I add to the editor at the start of the program, which has a fixed location (e.g. [-5000, -5000]). This node serves as a reference point of the internal coordinate system of the node editor. This works because dpg.get_item_state(ref_node)["rect_min"] contains the window-space position of the node, and you know the node editor-space coordinates of the node (i.e. [-5000, -5000] or whichever location you choose). Then you can simply calculate the offset between the window-space coordinates and the node editor-space coordinates of the node, which allows to convert between window-space and node editor-space positions.

The only problem I have with this is that it messes with the minimap of the node editor (i.e. it contains the far away reference node):

image

I'm guessing setting show=False in the reference node would solve this, but unfortunately that crashes DPG (#2151).

julioasotodv commented 11 months ago

I see, thanks @PhilippThoelke for the tip. Will try when I have some time!

PhilippThoelke commented 11 months ago

Actually this works quite well now. I set the reference node's position to the origin (0, 0) and configure its theme to be completely hidden (i.e. transparent background and empty label). I also set draggable=False to prevent users from moving the reference node. It shows up on the minimap as a tiny dot and doesn't really interfere with anything.

v-ein commented 10 months ago

Another workaround, instead of making the reference node transparent (which does have side effects on the minimap), would be to do a dpg.hide_item on that node - but first let it render for 1 frame. You can do it with set_frame_callback or other ways you want (e.g. item_visible_handler on the node editor, or split_frame if you create the node editor on the fly at runtime rather than on app initialization).

I'm going to push a PR for #2151 but it will take some time for the fix to go through all stages and get into a release.

PhilippThoelke commented 10 months ago

Thanks @v-ein! Hiding the node after one frame with set_frame_callback works but unfortunately breaks the workaround for determining the mouse position. It seems like dpg.get_item_state("_ref")["rect_min"] doesn't get updated for hidden nodes, therefore I can't determine the coordinate offset of the node editor.

v-ein commented 10 months ago

Played with node editor a bit... until this enhancement is implemented, using a reference node appears to be the only workaround. Also, as soon as you get a real node in your node editor, you can delete the dummy reference node and use any other node as a reference - but in this case, remember to restore the dummy node if you delete all the real nodes from the editor. This way you can keep your minimap unaffected by the dummy node.

Here's an example that uses the first node in the editor as a reference, no matter what that node contains. It works even if the user moves the first node around.

import dearpygui.dearpygui as dpg

dpg.create_context()
dpg.create_viewport(title=f"Test - {dpg.get_dearpygui_version()}", width=500, height=400)

dpg.setup_dearpygui()

with dpg.window(pos=(0, 30), width=500, height=350):
    dpg.add_button(label="Drag me!")
    with dpg.drag_payload(parent=dpg.last_item()):
        dpg.add_text("A new node")

    node_editor = dpg.generate_uuid()

    def on_drop():
        pos = dpg.get_mouse_pos(local=False)
        ref_node = dpg.get_item_children(node_editor, slot=1)[0]
        ref_screen_pos = dpg.get_item_rect_min(ref_node)
        ref_grid_pos = dpg.get_item_pos(ref_node)

        NODE_PADDING = (8, 8)

        pos[0] = pos[0] - (ref_screen_pos[0] - NODE_PADDING[0]) + ref_grid_pos[0]
        pos[1] = pos[1] - (ref_screen_pos[1] - NODE_PADDING[1]) + ref_grid_pos[1]

        with dpg.node(label="New", pos=pos, parent=node_editor):
            with dpg.node_attribute(attribute_type=dpg.mvNode_Attr_Static):
                dpg.add_text(f"I'm a new node")
                dpg.add_text(f"at {pos}!")

    with dpg.group(drop_callback=on_drop):
        with dpg.node_editor(tag=node_editor,
                             minimap=True,
                             minimap_location=dpg.mvNodeMiniMap_Location_BottomRight):

            with dpg.node(label="A real node", pos=[50, 30]):
                with dpg.node_attribute(attribute_type=dpg.mvNode_Attr_Static):
                    pass

dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()
monkeycc commented 5 months ago

Recently, I have also been researching But I can't achieve your effect Do you want to open source your demo examples thanks @julioasotodv