Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Individual mouse click callback for Node Editor grid & nodes #2289

Open
EssenOH opened this issue Feb 21, 2024 · 24 comments
Open

Individual mouse click callback for Node Editor grid & nodes #2289

EssenOH opened this issue Feb 21, 2024 · 24 comments
Labels
state: pending not addressed yet type: bug bug

Comments

@EssenOH
Copy link

EssenOH commented Feb 21, 2024

I'd like to implement right mouse click callbacks for 2 purposes and launch different popup(window) for selections.

firstly, the "node editor" has the callback for the right mouse click :

        # Add the actual node editor
        dpg.add_node_editor(tag=self._tag,
                            callback=self.__callbackAddLink,
                            delink_callback=self.callbackRemoveLink)

        # Regist callbacks for mouse & keyboard operations for node & link controls
        with dpg.handler_registry():
            dpg.add_mouse_click_handler(button=dpg.mvMouseButton_Right, callback=self.__callbackRightMouseClick)

secondary, the node has a dedicated callback for the right mouse click with popup:

    with dpg.node(tag=self._tag,
                  parent=editorHandle.tag,
                  label=self.nodeLabel,
                  pos=pos):
        with dpg.node_attribute(tag=self._attrImageInput.tag,
                                attribute_type=dpg.mvNode_Attr_Input,
                                shape=dpg.mvNode_PinShape_CircleFilled):
            dpg.add_image(tag=self._previewImageTag, texture_tag=self._previewTextureTag)
            with dpg.popup(parent=self._previewImageTag):
                dpg.add_text(default_value="image size", color=(232, 173, 9))

the problem is that the right mouse click calls only the "node editor" callback.

if I change the mode callback with the item handler with following APIs, the right mouse callback happens for both.

    with dpg.item_handler_registry(...) as handler:
        dpg.add_item_clicked_handler(...)

    dpg.bind_item_handler_registry(...)

the expected behavior is independent callbacks such as the right mouse click on node should call only the dedicated callback for node and the right mouse click on the "node editor" grid calls the callback for "node editor".

is there any way to register different callbacks for node & "node editor" with same mouse click ?

@EssenOH EssenOH added state: pending not addressed yet type: bug bug labels Feb 21, 2024
@v-ein
Copy link
Contributor

v-ein commented Feb 21, 2024

dpg.add_mouse_click_handler in your first example is not a node editor callback but a global handler. Please read up on handlers:

https://dearpygui.readthedocs.io/en/latest/documentation/io-handlers-state.html

@EssenOH
Copy link
Author

EssenOH commented Feb 21, 2024

if you are recommending me utilization of item hander like following, it generates binding error when it calls the dpg.bind_item_handler_registry(...) API, and then I couldn't figure it out.

        # Add the actual node editor
        dpg.add_node_editor(tag=self._tag,
                            callback=self.__callbackAddLink,
                            delink_callback=self.callbackRemoveLink)

        with dpg.item_handler_registry(tag="widget handler") as handler:
            dpg.add_item_clicked_handler(callback=self.__callbackRightMouseClick)

        dpg.bind_item_handler_registry(self._tag, "widget handler")

as I described in the previous post, the item handler is working well with a node, but it doesn't work with "node editor".
so, I am wondering why it doesn't work with referring the "node editor" tag.
it generates exception for the instantiation due to the binding API call.

Exception has occurred: SystemError
returned a result with an exception set
File "C:\Research\SHADOW\node_editor\editor.py", line 104, in init
dpg.bind_item_handler_registry(self._tag, "widget handler")
File "C:\Research\SHADOW\main.py", line 38, in main
editor = NodeEditor(settings=settings,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Research\SHADOW\main.py", line 61, in
main()
SystemError: returned a result with an exception set

@v-ein
Copy link
Contributor

v-ein commented Feb 21, 2024

Can you post the entire error message? There should be more diagnostic lines above that "Exception has occurred" line.

@EssenOH
Copy link
Author

EssenOH commented Feb 21, 2024

Here is the screen capture :
image

@v-ein
Copy link
Contributor

v-ein commented Feb 21, 2024

What kind of IDE is this? Can you run your application from the command line?

@v-ein
Copy link
Contributor

v-ein commented Feb 21, 2024

It definitely skips the diagnostics; theres should be either a console window in the IDE, or you can run it from command line and see full output there.

@EssenOH
Copy link
Author

EssenOH commented Feb 22, 2024

CLI produces more information :
image

The IDE is "Visual Studio Code" and unfortunately it doesn't show the message as CLI if user push the F5 button to run.

@v-ein
Copy link
Contributor

v-ein commented Feb 22, 2024

VS Code typically opens a Terminal panel at the bottom of the window, which is identical to CLI (you can even type commands there). Not sure if it's turned off or somehow suppressed in your installation.

Regarding the error, take a look at the "Message:" line. That's about it. For some reason the node editor does not support the "clicked" handler. Personally I don't know the reason.

A typical way to circumvent it is like in this comment, use a global click handler (the one you have in dpg.handler_registry()), and check if the node editor is hovered during that click. Without seeing your __callbackRightMouseClick, I'm not sure if you're doing it this way or not.

Now let's check whether I understand your problem correctly:

  1. You handle clicks on the node editor via a global click handler (dpg.handler_registry + dpg.add_mouse_click_handler).
  2. You also handle clicks on individual nodes via the item's click handler (add_item_clicked_handler).
  3. The problem is that when you click on a node, both handlers are called, not just the node click handler. Correct?

@EssenOH
Copy link
Author

EssenOH commented Feb 22, 2024

current my environment & situation is like following :

  1. dearpygui 1.10.1, native install instead of anaconda, windows system.
  2. I could type command on VS Code terminal and get the same error message as CLI case.
  3. I used F5 button for the application running and it suppress the debugging message ( the diagnostics )

there are 2 problem cases here :
firstly, your understanding is correct and the both handlers are called if I use the item click handler for the individual nodes.
so, my understanding is that the utilization of global click handler for node editor is not a right approaching and it should provide the item click handler.

secondary, if I use "with dpg.popup(parent=self._previewImageTag):" for the individual nodes, the right click on individual node doesn't launch popup window and it calls only the global click handler.

@v-ein
Copy link
Contributor

v-ein commented Feb 22, 2024

The latter works fine for me. I've just tried it on an image within a node, and the popup does show up. Except that first time it's shown in the top left corner of the window, which is probably caused by #1691.

with dpg.node(label="Node 1", pos=(20, 20)):
    with dpg.node_attribute(label="Attr", attribute_type=dpg.mvNode_Attr_Output) as attr1:
        dpg.add_text("Text")
        width, height, channels, fg_data = dpg.load_image(str(Path(__file__).parent / "_transparent.png"))
        with dpg.texture_registry(show=False):
            dpg.add_static_texture(width=width, height=height, default_value=fg_data, tag="fg_image")
        img = dpg.add_image("fg_image")
        with dpg.popup(img, min_size=(0, 0)):
            dpg.add_menu_item(label="Hey there")

@EssenOH
Copy link
Author

EssenOH commented Feb 22, 2024

for the latter one, actually it doesn't work if you combine it with a global click handler (dpg.handler_registry + dpg.add_mouse_click_handler).

It just popup only the window for global handler and it doesn't show the popup for the nodes.

import dearpygui.dearpygui as dpg
from pathlib import Path

editorTag = 9999
node1Tag = 10000
node2Tag = 10001
popupTag = 10002

dpg.create_context()

# callback runs when user attempts to connect attributes
def link_callback(sender, app_data):
	# app_data -> (link_id1, link_id2)
	dpg.add_node_link(app_data[0], app_data[1], parent=sender)

# callback runs when user attempts to disconnect attributes
def delink_callback(sender, app_data):
	# app_data -> link_id
	dpg.delete_item(app_data)

def __callbackRightMouseClick(self):
	dpg.set_item_pos(popupTag, dpg.get_mouse_pos(local=False))
	dpg.configure_item(popupTag, show=True)

with dpg.window(tag=popupTag, label="Right click Pop-Up Window", modal=False, popup=True, show=False, no_title_bar=True):
	dpg.add_text(default_value="Nodes", color=(232, 173, 9))

with dpg.window(label="Tutorial", width=400, height=400):

	with dpg.node_editor(tag=editorTag, callback=link_callback, delink_callback=delink_callback):
		with dpg.node(tag=node1Tag, parent=editorTag, label="Node 1"):
			with dpg.node_attribute(label="Node A1"):
				dpg.add_input_float(label="F1", width=150)

			with dpg.node_attribute(label="Node A2", attribute_type=dpg.mvNode_Attr_Output):
				dpg.add_input_float(label="F2", width=150)

		with dpg.node(tag=node2Tag, parent=editorTag, label="Node 2"):
			with dpg.node_attribute(label="Attr", attribute_type=dpg.mvNode_Attr_Output) as attr1:
				dpg.add_text("Text")
				width, height, channels, fg_data = dpg.load_image(str(Path(__file__).parent / "_transparent.png"))
				with dpg.texture_registry(show=False):
					dpg.add_static_texture(width=width, height=height, default_value=fg_data, tag="fg_image")
				img = dpg.add_image("fg_image")
				with dpg.popup(img, min_size=(0, 0)):
					dpg.add_menu_item(label="Hey there")

	with dpg.handler_registry():
		dpg.add_mouse_click_handler(button=dpg.mvMouseButton_Right, callback=__callbackRightMouseClick)


dpg.create_viewport(title='Custom Title', width=800, height=600)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()

@v-ein
Copy link
Contributor

v-ein commented Feb 23, 2024

Well, since your global callback fires even if you click on the node, it shows the popupTag window in that case, too. Two popups can't be displayed at once. In your case, popupTag wins the race and gets displayed, while the popup for the node gets reverted back to hidden state.

To handle that properly, you need to determine whether a node was clicked, and ignore the global click in this case. If you have relatively few nodes, you can just iterate over them and see if one of them is hovered:

def on_click_global():
    if dpg.is_item_hovered(editor):
        for node in dpg.get_item_children(editor, slot=1):
            if dpg.is_item_hovered(node):
                print("Ignoring global click")
                return
        print("Editor clicked")

so, my understanding is that the utilization of global click handler for node editor is not a right approaching and it should provide the item click handler.

I agree that it would be nice to have an item_clicked handler on the node editor, but this doesn't necessarily means that clicks won't "fall through" to the node editor. There are no explicit rules in Dear ImGui and DPG on how to handle such situations, and implementation-wise it might be easier to make them fall through (is_item_hovered is a good example of this).

@EssenOH
Copy link
Author

EssenOH commented Feb 23, 2024

My workaround idea was also a way checking the nodes in the global handler, but I couldn't make sure that it can ignore the event & defer the mouse click event to node's event and how to do it.

also, I didn't know about the API call dpg.get_item_children(editor, slot=1) can return the node instance to check the status ( actually, this is the key information than is_tem_hovered(...) API ).

BTW, I still believe that the node editor should have the item_clicked handler because it is also an item which can be referred with dpg.is_item_hovered(editor) API.

With the workaround, it can allow me to use right click handlers for node editor & nodes, that is great...thanks for your detail explanations and the workaround snippet code.

Additionally, still there is a weird behavior and then I am trying to narrow down the case.
it is likely...

  1. select the node which has an item handler.
  2. right-clicking the node, it shows the popup(created with dpg.popup(...)) for node
  3. right-clicking the node editor, shows the popup( created with dpg.window(...)) for node editor

up to here it is an expected behavior

  1. right-clicking anywhere, it shows the only popup for node editor

I think the problem is because a user reaction for the node editor popup was not done and the state-machine of the node editor popup is still waiting for user action.
so, do you have any idea to reset it ?
currently, if user do an extra action like "left-click" on anywhere, it resets the state-machine of the node editor popup.

@v-ein
Copy link
Contributor

v-ein commented Feb 23, 2024

I think it's because your global click handler opens a popup even if it is already displayed, where as dpg.popup uses the subsequent click to close the popup.

Since you know the popup ID of your node editor popup, you can try to check if it's visible and don't bring it again in this case:

def on_click_global():
    if dpg.is_item_hovered(editor) and not dpg.is_item_visible(popupTag):

Give it a try and see. Depending on the exact sequence of events it might not work; in that case I can try to think out a better solution.

@EssenOH
Copy link
Author

EssenOH commented Feb 23, 2024

I just tried and it is not helpful and couldn't solve the issue.
I also tried calling dpg.configure_item(self._popupTag, show=False) to make invisible, but it also doesn't help me.

@v-ein
Copy link
Contributor

v-ein commented Feb 24, 2024

Maybe I misunderstood your problem with popup menus. I've just re-read it and I don't really get it what the problem is in step 4.

For me, popups work just fine:

popups

Here's the script I used:

from pathlib import Path
import dearpygui.dearpygui as dpg

dpg.create_context()
with dpg.window(popup=True, show=False, min_size=(0, 0)) as popup_wnd:
    dpg.add_text("Editor popup")

with dpg.window(label="Example Window") as main:
    with dpg.node_editor() as editor:
        with dpg.node(label="Node 1", pos=(100, 100)):
            with dpg.node_attribute(label="Attr", attribute_type=dpg.mvNode_Attr_Output) as attr1:
                width, height, channels, fg_data = dpg.load_image(str(Path(__file__).parent / "handshake.png"))
                with dpg.texture_registry(show=False):
                    dpg.add_static_texture(width=width, height=height, default_value=fg_data, tag="fg_image")
                img = dpg.add_image("fg_image")
                with dpg.popup(img, min_size=(0, 0)):
                    dpg.add_menu_item(label="Node popup")

        def on_click_global():
            if dpg.is_item_hovered(editor):
                for node in dpg.get_item_children(editor, slot=1):
                    if dpg.is_item_hovered(node):
                        print("Ignoring global click")
                        return
                print("Editor clicked")
                dpg.show_item(popup_wnd)

        with dpg.handler_registry() as handlers:
            dpg.add_mouse_click_handler(callback=on_click_global)

dpg.create_viewport(width=500, height=500)
dpg.setup_dearpygui()

dpg.set_primary_window(main, True)

dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()

@EssenOH
Copy link
Author

EssenOH commented Feb 25, 2024

After click on any place, regardless on the node or node editor, your animation is always doing another extra click to hide the popup, and then the dedicated proper popup is showing for next click and it shows only node editor popup for next clicks.

could you right-click the node and directly right-click on node editor area and right-click once again on node ?
so, please eliminate the extra click to hide the popup regardless node or node editor.
the 1st and 2nd click shows the right pop up, but 3rd click shows only node editor popup.

My question was how I can replace the "extra click" with programmable way to show the right popups from the 3rd click.

@v-ein
Copy link
Contributor

v-ein commented Feb 25, 2024

could you right-click the node and directly right-click on node editor area and right-click once again on node ?

Something like this?
popups-2

My question was how I can replace the "extra click" with programmable way to show the right popups from the 3rd click.

So you want it to work like this - correct?

  1. Click on the node: the popup for the node opens.
  2. Without touching anything or pressing Esc, click on the node editor area - the node popup disappears, and at the same time the node editor popup opens, without any extra clicks.

@EssenOH
Copy link
Author

EssenOH commented Feb 26, 2024

Yes, in order to give user more intuitive operations for new node adding and configuration of node parameters with minimum clicking and then user can skip the actual selection for the popups.

@v-ein
Copy link
Contributor

v-ein commented Feb 26, 2024

Something like this?

popups-3

from pathlib import Path
import dearpygui.dearpygui as dpg


dpg.create_context()
with dpg.window(popup=True, show=False, min_size=(0, 0)) as editor_popup:
    dpg.add_text("Editor popup")

with dpg.window(popup=True, show=False, min_size=(0, 0)) as node_popup:
    dpg.add_text("Node popup")

with dpg.window(label="Example Window") as main:
    with dpg.node_editor() as editor:
        with dpg.node(label="Node 1", pos=(100, 100)):
            with dpg.node_attribute(label="Attr", attribute_type=dpg.mvNode_Attr_Output) as attr1:
                width, height, channels, fg_data = dpg.load_image(str(Path(__file__).parent / "handshake.png"))
                with dpg.texture_registry(show=False):
                    dpg.add_static_texture(width=width, height=height, default_value=fg_data, tag="fg_image")
                img = dpg.add_image("fg_image")

        def on_click_global():
            # Note: could as well test for `dpg.get_active_window() in (editor_popup, node_popup)`
            if dpg.get_item_configuration(dpg.get_active_window())['popup']:
                # Let the popup close
                dpg.split_frame()

            if dpg.is_item_hovered(editor):
                for node in dpg.get_item_children(editor, slot=1):
                    if dpg.is_item_hovered(node):
                        print("Node clicked")
                        dpg.show_item(node_popup)
                        return
                print("Editor clicked")
                dpg.show_item(editor_popup)

        with dpg.handler_registry() as handlers:
            dpg.add_mouse_click_handler(button=dpg.mvMouseButton_Right, callback=on_click_global)

dpg.create_viewport(width=500, height=500)
dpg.setup_dearpygui()

dpg.set_primary_window(main, True)

dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()

@v-ein
Copy link
Contributor

v-ein commented Feb 26, 2024

All clicks in the video are right-clicks. A left-click will simply close the popup without bringing up a new one.

@EssenOH
Copy link
Author

EssenOH commented Feb 26, 2024

Ya, it is a very similar to desired behavior, but it is not perfectly matching to my looking point.

The implementation should be different way instead of processing all of the operations within a global handler.
Each node should have it's own handler for each purpose and then I think assigning the callback for each node with item_clicked handler is the best.

With the the context, the example is good enough to show the global handler can treat either node editor and nodes, but it is not good enough to split the operations for each node and node editor.

@v-ein
Copy link
Contributor

v-ein commented Mar 1, 2024

If you want to have individual handlers for nodes, you'll have to dispatch events to local handlers on your own.

Here's the problem: a local item_clicked handler only fires when the item gets clicked. From Dear ImGui's point of view, this means: (1) the click occurs within the item's boundaries and (2) its parent window is active. When a popup is displayed, the popup itself is a window and it is active. This means only items within the popup will receive the "clicked" events. Nodes won't receive clicks until its parent window becomes active (which happens when popup is closed). That's why local handlers cannot catch the click that closes the popup.

@v-ein
Copy link
Contributor

v-ein commented Mar 1, 2024

The global handler receives all clicks and therefore is able to determine the moment when the popup closes and the click is on a node widget.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
state: pending not addressed yet type: bug bug
Projects
None yet
Development

No branches or pull requests

2 participants