DearPyGui icon indicating copy to clipboard operation
DearPyGui copied to clipboard

Programmatic API to trigger when wait_for_input=True

Open sistemicorp opened this issue 1 year ago • 5 comments

Use Case: When the app is configured with,

dpg.configure_app(wait_for_input=True)

Provide an API so that the condition of user input can be simulated (or otherwise "triggered").

Describe the solution you'd like Possible examples of the API call could be,

dpg.configure_app(input_now=True)
dpg.wait_for_input_trigger()

Describe alternatives you've considered As a workaround, I am using the pynput module and simulating mouse activity,

from pynput.mouse import Controller
mouse = Controller()
mouse.move(0, 0)

And this works for me. Tested on RPi (DietPi 8), Linux (Ubuntu 22) and Windows 10.

Additional context Why am I using wait_for_input in the first place? I don't know why wait_for_input feature is present, and perhaps my use case is different than the intended purpose, so its worth describing my use case. I am plotting many many (>1M) data points on a plot, and what happens is the frame rate, initially at 60fps, drops to <20 fps as data points build up beyond >1M. For this application I am not interested in frame rate. The plotted data is essentially static, updated at 1Hz. When the frame rate drops the system becomes very slow and user interaction becomes very laggy and the GUI becomes unusable. And the poor PC fan runs high. By using the wait_for_input the processing is cut way back and the GUI remains responsive. But I needed a way to tell DPG that the plot data has been updated, so I create the fake mouse event.

In the larger picture, it might be better to have an API that allows certain widgets to be "recalculated" only when needed. In other words wait_for_input is applied per item (that has the capability) rather than globally. For example,

dpg.configure_item(tag="myplot", wait_for_input=True);
...
dog.set_value(tag="myplot", (updated_x, updated_y));
dpg.configure_item(tag="myplot", update=True);

It was mentioned on the discord channel that this idea could be extended do the node editor.

sistemicorp avatar Jul 07 '22 15:07 sistemicorp

Note: a drawback to using the mouse event to fake a wait_for_input is that if the mouse is not over the application the faked mouse event does not get sent to the (DPG) application. In my case this cases the app to stall and not be usable.

I will need a better workaround...

sistemicorp avatar Jul 17 '22 19:07 sistemicorp

Template for a work around using the render loop.

from threading import Timer, Lock
import dearpygui.dearpygui as dpg

dpg.create_context()
dpg.create_viewport(title='Custom Title', width=600, height=300)

with dpg.window(label="Example Window", tag="mywin", width=300):
    dpg.add_text("Hello world", tag="mytxt")

# --------------------------------------------

_lock = Lock()
_lock.acquire()


def _unlock_dpg():
    if _lock.locked():
        _lock.release()


def _handler_unlock_dpg(sender, app_data):
    _unlock_dpg()


with dpg.handler_registry():
    dpg.add_mouse_move_handler(callback=_handler_unlock_dpg)
    dpg.add_mouse_click_handler(callback=_handler_unlock_dpg)
    dpg.add_mouse_double_click_handler(callback=_handler_unlock_dpg)
    # add other user input types...

# --------------------------------------------

# this timer loop emulates program updating widgets
count = 0
def _timer_cb():
    global count
    count += 1
    print("timer")

    dpg.configure_item("mywin", label=f"Example Window {count}")
    dpg.set_value("mytxt", value=f"Hell World {count}")

    # programatically allow dpg to render, after changing widgets
    _unlock_dpg()

    Timer(1, _timer_cb).start()


dpg.show_metrics()
dpg.setup_dearpygui()
dpg.show_viewport()

_timer_cb()  # let the games begin

while dpg.is_dearpygui_running():
    locked = _lock.acquire(blocking=False)
    if locked:
        dpg.render_dearpygui_frame()

dpg.destroy_context()

sistemicorp avatar Jul 19 '22 14:07 sistemicorp

Perhaps there is a glfw solution per the docs, Event Processing section.

Specifically,

If the main thread is sleeping in glfwWaitEvents, you can wake it from another thread by posting an empty event to the event queue with glfwPostEmptyEvent.

So, maybe one of these,

dpg.wait_for_input_render()  # which is a wrapper for glfwPostEmptyEvent
dpg.wait_for_input_unblock()
dpg.wait_for_input_post()

Also note there is glfwWaitEventsTimeout. Perhaps there could also be,

dpg.configure_app(wait_for_event_timeout=timeout_ms)

sistemicorp avatar Jul 21 '22 20:07 sistemicorp

Spent an afternoon trying a solution tho this problem but failed. I thought I would detail what I tried in case someone can suggest something. The Linux platform was used for this attempted solution.

When wait_for_input is set, the impact on the render loop is here, in mv_internal void mvPrerender() where we have this segment,

    if (GContext->IO.waitForInput)
        glfwWaitEvents();
    else
        glfwPollEvents();

Changes to various files were made to create a function that lives in mvViewport_linux.cpp, in this case the function is mv_impl void mvWaitForInputRender(mvViewport& viewport). Within this function I set a state variable that would bypass the above call to glfwWaitEvents(), but this did not have the affect of rendering.

In some particular attempts, if glfwWaitEvents() was skipped for multiple frames (DPG render loop), then wait_for_input would seemed to be disabled, and no longer blocking rendering and the frame rate would be ~60fps.

As stated above, the glfw docs seem to suggest that when glfwWaitEvents(); is used, glfwPostEmptyEvent(); can be used to unblock glfw. But I was unable to make that work. There is this bug report on glfwPostEmptyEvent(); but the fix seems to have been merged, and I confirmed I am running glfw ver 3.3.7.

I tried calling glfwSwapBuffers(), and that didn't trigger a window update.

sistemicorp avatar Jul 29 '22 21:07 sistemicorp

Minimal example to test new feature,

from threading import Timer
import dearpygui.dearpygui as dpg
import time

# Expected Result:
# - DPG frame rate is <5 fps when there is no user input
# - the items mywin, mytxt are updated at 1Hz (with no user input)

_tmr = None

dpg.create_context()
dpg.create_viewport(title='Custom Title', width=600, height=300)
dpg.configure_app(wait_for_input=True)

with dpg.window(label="Example Window", tag="mywin", width=300):
    dpg.add_text("Hello world", tag="mytxt")

count = 0
def _timer_cb():  # timer function emulates program updating an item
    global _tmr
    if _tmr:
        _tmr.cancel()
        _tmr = None

    global count
    count += 1

    if not dpg.is_dearpygui_running():
        return

    # programmatically change item value/configuration
    dpg.configure_item("mywin", label=f"Example Window {count}")
    dpg.set_value("mytxt", value=f"Hello World {count}")

    # programmatically update/rerender the changed items
    # >> place new feature function that causes window refresh/update/render here <<
    
    # toggling wait_for_input does not work, DPG framerate is 60fps
    #dpg.configure_app(wait_for_input=False)
    #time.sleep(0.02)  # has no impact
    #dpg.configure_app(wait_for_input=True)

    _tmr = Timer(1.0, _timer_cb)
    _tmr.start()
    print("timer")


_tmr = Timer(1, _timer_cb)
_tmr.start()

dpg.setup_dearpygui()

dpg.show_metrics()

dpg.show_viewport()
dpg.start_dearpygui()

if _tmr:
    _tmr.cancel()
    _tmr = None

dpg.destroy_context()

sistemicorp avatar Aug 02 '22 14:08 sistemicorp

We are reviewing issues to see if they can be closed and I am leaning towards closing this issue. Is your last comment a workaround? Is this feature still 100% required?

bandit-masked avatar Jan 04 '23 22:01 bandit-masked

I was unable to solve this issue, or implement a workaround that worked well enough. But the issue can be closed.

The problem with the workaround in the initial post (using a fake mouose event) is that when the cursor is off the main DPG window, the mouse event is not sent to the DPG app, and therefore the "GUI event" is not scene.

sistemicorp avatar Jan 05 '23 01:01 sistemicorp