ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
61.01k stars 10.28k forks source link

Key stuck when launching a native dialog in Windows #7264

Open rcases opened 9 months ago

rcases commented 9 months ago

Version/Branch of Dear ImGui:

docking

Back-ends:

sokol

Compiler, OS:

Windows 10

Full config/build information:

Dear ImGui 1.90.2 WIP (19015)
--------------------------------
sizeof(size_t): 4, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=201703
define: _WIN32
define: __clang_version__=5.0.2 (5afbca23.5b3e5c70.38175)
define: IMGUI_HAS_VIEWPORT
define: IMGUI_HAS_DOCK
--------------------------------
io.BackendPlatformName: NULL
io.BackendRendererName: NULL
io.ConfigFlags: 0x00000040
 DockingEnable
io.ConfigViewportsNoDecoration
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x0000000A
 HasMouseCursors
 RendererHasVtxOffset
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,64
io.DisplaySize: 1024.00,768.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00

Details:

Key stuck when launching a native dialog in Windows

When I start a native dialog in Windows using a shortcut, the key gets stuck until I press it again. This happens because the dialog box is opened and closed in the same frame, the ImGuiInputEventType_Focus event is overridden, and g.IO.ClearInputKeys() is not called in ImGui::UpdateInputEvents(…). As a workaround you can use this code

    if (ImGui::Shortcut(ImGuiMod_Shortcut | ImGuiKey_O, 0, ImGuiInputFlags_RouteAlways) == true)
    {
        ImGui::GetIO().ClearInputKeys();
        MessageBox(NULL, "Native Dialog", "Test", MB_OK);
    }

Screenshots/Video:

Animation

Minimal, Complete and Verifiable Example code:

// Here's some code anyone can copy and paste to reproduce your issue
    if (ImGui::Shortcut(ImGuiMod_Shortcut | ImGuiKey_O, 0, ImGuiInputFlags_RouteAlways) == true)
    {
        MessageBox(NULL, "Native Dialog", "Test", MB_OK);
    }
ocornut commented 9 months ago

You may also surround your model call with io.SetAppAcceptingEvents(false) then back to true.

Perhaps we should detect Focus Lost/Gained followed by long delta time and treat that specially, but I would worry this would alter with stepping in a debugging..

rcases commented 9 months ago

You may also surround your model call with io.SetAppAcceptingEvents(false) then back to true.

Perhaps we should detect Focus Lost/Gained followed by long delta time and treat that specially, but I would worry this would alter with stepping in a debugging..

io.SetAppAcceptingEvents(false) has not worked for me. The ImGuiIO::AddFocusEvent(...) function is not affected by the AppAcceptingEvents variable. But it wouldn't work either, since both the WM_KILLFOCUS and WM_SETFOCUS events would be lost.

Another way to fix the issue would be to see if the window that gains focus is of type viewport or another type, but I suppose this would require important changes in the core and backends.

ocornut commented 9 months ago

io.SetAppAcceptingEvents(false) has not worked for me.

That's for another thing: with some modal dialogs you may still receive events which will queue up while the modal is open. If that happen you can use io.SetAppAcceptingEvents(false), io.SetAppAcceptingEvents(true) for this purpose.

If I search for my work logs for when I added SetAppAcceptingEvents(), I noticed that a simple situation like this:

ImGui::Begin("Test", NULL, ImGuiWindowFlags_MenuBar);
if (ImGui::BeginMenuBar())
{
    if (ImGui::MenuItem("Blocking Win32 dialog"))
    {
        char buf[256] = "";
        OPENFILENAMEA fn = {};
        fn.lStructSize = sizeof(fn);
        fn.hwndOwner = NULL;
        fn.lpstrFile = buf;
        fn.nMaxFile = sizeof(buf);
        fn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
        ::GetOpenFileNameA(&fn);
    }
    ImGui::EndMenuBar();
}
ImGui::End();

You can STILL manually refocus our window and messages/events will be accumulated into the queue.

But it wouldn't work either, since both the WM_KILLFOCUS and WM_SETFOCUS events would be lost.

Interesting, you can right that combining both you are likely to lose the focus events. Since we added ClearEventsQueue() afterwards, it may be simpler to add ClearEventsQueue() when exiting the modal such as Win32 ::MessageBox() or ::GetOpenFileName().

Another way to fix the issue would be to see if the window that gains focus is of type viewport or another type, but I suppose this would require important changes in the core and backends.

Our handling of AddFocusEvent(false)/AddFocusEvent(true) in same frame is designed to effectively handle that, but it indeed fail when a modal/blocking loop is involved.

TL;DR; I think the right solution is:

io.SetAppAcceptingEvents(false);
::MessageBox(NULL, "Native Dialog", "Test", MB_OK);
io.ClearInputKeys();
io.ClearEventsQueue();
io.SetAppAcceptingEvents(true);

Technically both ClearXXX calls could be moved above the modal and it shouldn't change a thing.

rcases commented 9 months ago

One interesting thing to note is that if the native window owner is NULL, the parent window can still be selected and a second label appears in the taskbar for the new window, which is annoying.

To avoid this, you can put the Handle of the main window in the case of my example, it would be:

io.SetAppAcceptingEvents(false);
::MessageBox(find_main_window(GetCurrentProcessId()), "Native Dialog", "Test", MB_OK);
io.ClearInputKeys();
io.ClearEventsQueue();
io.SetAppAcceptingEvents(true);

Being find_main_window, the code found in: find_main_window

This way the main window message queue is almost completely stopped and there are no dependencies on the backend window manager.

iwubcode commented 9 months ago

Not sure this is valuable but for another data point.

This occurred to me in an application I am developing where I was arrowing down in a large tree of nodes. Unexpectedly (due to an error in the program unrelated to the GUI part of the application) a message box appeared and took away focus. Closing the box, imgui continually treated the arrow down event as occurring.

I'm not sure if SetAcceptingEvents is relevant in such a situation (maybe?). I was able to solve the error that caused the message box to occur and thus work around the issue.

ocornut commented 9 months ago

a message box appeared and took away focus. Closing the box, imgui continually treated the arrow down event as occurring.

Normally if your imgui loop is running you should be getting the focus lost event which clears inputs? Not sure what was preventing that?